Files
gridpilot.gg/scripts/generate-templates/index.ts

254 lines
6.8 KiB
TypeScript

#!/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<void> {
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<CaptureResult> {
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<FixtureResult> {
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<CaptureResult[]> {
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<void> {
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);
});