#!/usr/bin/env npx tsx /** * Template Generation Script * * Generates PNG templates from HTML fixtures using Playwright. * These templates are used for image-based UI matching in OS-level automation. * * Usage: npx tsx scripts/generate-templates/index.ts */ import { chromium, type Browser, type Page } from 'playwright'; import * as fs from 'fs'; import * as path from 'path'; import { SELECTOR_CONFIG, COMMON_CAPTURES, TEMPLATE_BASE_PATH, FIXTURES_BASE_PATH, type ElementCapture, type FixtureConfig, } from './SelectorConfig'; const PROJECT_ROOT = process.cwd(); interface CaptureResult { outputPath: string; success: boolean; error?: string; } interface FixtureResult { htmlFile: string; captures: CaptureResult[]; } async function ensureDirectoryExists(filePath: string): Promise { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); console.log(` Created directory: ${dir}`); } } async function captureElement( page: Page, capture: ElementCapture, outputBasePath: string ): Promise { const fullOutputPath = path.join(outputBasePath, capture.outputPath); try { await ensureDirectoryExists(fullOutputPath); const element = await page.locator(capture.selector).first(); const isVisible = await element.isVisible().catch(() => false); if (!isVisible) { console.log(` ⚠ Element not visible: ${capture.description}`); return { outputPath: capture.outputPath, success: false, error: 'Element not visible', }; } await element.screenshot({ path: fullOutputPath }); console.log(` āœ“ Captured: ${capture.description} → ${capture.outputPath}`); return { outputPath: capture.outputPath, success: true, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.log(` āœ— Failed: ${capture.description} - ${errorMessage}`); return { outputPath: capture.outputPath, success: false, error: errorMessage, }; } } async function processFixture( browser: Browser, config: FixtureConfig, fixturesBasePath: string, outputBasePath: string ): Promise { const htmlPath = path.join(fixturesBasePath, config.htmlFile); const fileUrl = `file://${htmlPath}`; console.log(`\nšŸ“„ Processing: ${config.htmlFile}`); if (!fs.existsSync(htmlPath)) { console.log(` āœ— File not found: ${htmlPath}`); return { htmlFile: config.htmlFile, captures: config.captures.map((c) => ({ outputPath: c.outputPath, success: false, error: 'HTML file not found', })), }; } const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, }); const page = await context.newPage(); try { await page.goto(fileUrl, { waitUntil: 'networkidle' }); await page.waitForTimeout(1000); const captures: CaptureResult[] = []; for (const capture of config.captures) { const result = await captureElement(page, capture, outputBasePath); captures.push(result); } return { htmlFile: config.htmlFile, captures, }; } finally { await context.close(); } } async function captureCommonElements( browser: Browser, fixturesBasePath: string, outputBasePath: string ): Promise { console.log('\nšŸ“¦ Capturing common elements...'); const sampleFixture = SELECTOR_CONFIG.find((c) => fs.existsSync(path.join(fixturesBasePath, c.htmlFile)) ); if (!sampleFixture) { console.log(' āœ— No fixture files found for common element capture'); return COMMON_CAPTURES.map((c) => ({ outputPath: c.outputPath, success: false, error: 'No fixture files available', })); } const htmlPath = path.join(fixturesBasePath, sampleFixture.htmlFile); const fileUrl = `file://${htmlPath}`; const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, }); const page = await context.newPage(); try { await page.goto(fileUrl, { waitUntil: 'networkidle' }); await page.waitForTimeout(1000); const captures: CaptureResult[] = []; for (const capture of COMMON_CAPTURES) { const result = await captureElement(page, capture, outputBasePath); captures.push(result); } return captures; } finally { await context.close(); } } async function main(): Promise { console.log('šŸš€ Starting template generation...\n'); const fixturesBasePath = path.join(PROJECT_ROOT, FIXTURES_BASE_PATH); const outputBasePath = path.join(PROJECT_ROOT, TEMPLATE_BASE_PATH); console.log(`šŸ“ Fixtures path: ${fixturesBasePath}`); console.log(`šŸ“ Output path: ${outputBasePath}`); if (!fs.existsSync(fixturesBasePath)) { console.error(`\nāŒ Fixtures directory not found: ${fixturesBasePath}`); process.exit(1); } await ensureDirectoryExists(path.join(outputBasePath, '.gitkeep')); console.log('\n🌐 Launching browser...'); const browser = await chromium.launch({ headless: true, }); try { const results: FixtureResult[] = []; for (const config of SELECTOR_CONFIG) { const result = await processFixture( browser, config, fixturesBasePath, outputBasePath ); results.push(result); } const commonResults = await captureCommonElements( browser, fixturesBasePath, outputBasePath ); console.log('\nšŸ“Š Summary:'); console.log('─'.repeat(50)); let totalCaptures = 0; let successfulCaptures = 0; for (const result of results) { const successful = result.captures.filter((c) => c.success).length; const total = result.captures.length; totalCaptures += total; successfulCaptures += successful; console.log(` ${result.htmlFile}: ${successful}/${total} captures`); } const commonSuccessful = commonResults.filter((c) => c.success).length; totalCaptures += commonResults.length; successfulCaptures += commonSuccessful; console.log(` common elements: ${commonSuccessful}/${commonResults.length} captures`); console.log('─'.repeat(50)); console.log(` Total: ${successfulCaptures}/${totalCaptures} captures successful`); if (successfulCaptures < totalCaptures) { console.log('\n⚠ Some captures failed. This may be due to:'); console.log(' - Elements not present in the HTML fixtures'); console.log(' - CSS selectors needing adjustment'); console.log(' - Dynamic content not rendering in static HTML'); } console.log('\nāœ… Template generation complete!'); console.log(` Templates saved to: ${outputBasePath}`); } finally { await browser.close(); } } main().catch((error) => { console.error('\nāŒ Fatal error:', error); process.exit(1); });