254 lines
6.8 KiB
TypeScript
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);
|
|
}); |