working companion prototype
This commit is contained in:
254
scripts/generate-templates/index.ts
Normal file
254
scripts/generate-templates/index.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user