/** * Playwright test recorder — generate Playwright test code from crawl data. * * Given a list of crawled screens, generates a minimal Playwright test file that: * 1. Navigates to each screen URL * 2. Verifies the page title * 3. Checks key elements visible on the page * 4. Verifies no console errors (optional) * * This is a "smoke test" generator — not a full interaction recorder. * Output: valid TypeScript Playwright test file ready to run with npx playwright test. */ export interface RecorderScreen { id: string; name: string; url: string; title?: string; heading?: string; markdownWordCount?: number; smartLocator?: { tag: string; // e.g. 'button', 'a', 'input' text?: string; // visible text content role?: string; // ARIA role placeholder?: string; // for inputs nearText?: string; // nearby label text }; } export interface RecorderOpts { baseUrl?: string; includeConsoleCheck?: boolean; testTimeout?: number; // ms, default 30000 projectName?: string; useSmartLocators?: boolean; // default true } function buildSmartLocatorHelper(): string { return ` // smartLocator — falls back to similarity search when primary selector fails. // Generated by ZeTa Crawler. Do not edit manually. async function smartLocator(page: any, primary: string, ref: { tag?: string; text?: string; role?: string; placeholder?: string }) { try { const el = page.locator(primary); if (await el.count() > 0) return el; } catch { /* fall through */ } // Fallback: find by ARIA role + text if (ref.role && ref.text) { const byRole = page.getByRole(ref.role as any, { name: ref.text, exact: false }); if (await byRole.count() > 0) return byRole; } // Fallback: find by text content if (ref.text) { const byText = page.getByText(ref.text, { exact: false }); if (await byText.count() > 0) return byText; } // Fallback: find by placeholder if (ref.placeholder) { const byPlaceholder = page.getByPlaceholder(ref.placeholder, { exact: false }); if (await byPlaceholder.count() > 0) return byPlaceholder; } // Last resort: return the primary (will fail with original error) return page.locator(primary); }`.trim(); } function sanitizeTestName(name: string): string { return name.replace(/[^a-zA-Z0-9 _-]/g, '').trim().slice(0, 80) || 'Screen'; } function escapeString(s: string): string { return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/`/g, '\\`'); } export function generatePlaywrightTest(screens: RecorderScreen[], opts: RecorderOpts = {}): string { const { includeConsoleCheck = false, testTimeout = 30_000, projectName = 'ZeTa Crawl', useSmartLocators = true, } = opts; const needsSmartLocator = useSmartLocators && screens.some(s => s.smartLocator != null); const lines: string[] = [ `// Generated by ZeTa Crawler — ${new Date().toISOString()}`, `// Project: ${escapeString(projectName)}`, `// Screens: ${screens.length}`, `// Run: npx playwright test`, '', "import { test, expect } from '@playwright/test';", '', ]; if (needsSmartLocator) { lines.push(buildSmartLocatorHelper(), ''); } lines.push(`test.setTimeout(${testTimeout});`, ''); if (includeConsoleCheck) { lines.push( `test.beforeEach(async ({ page }) => {`, ` const errors: string[] = [];`, ` page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); });`, ` page.on('pageerror', err => errors.push(err.message));`, ` (page as any).__consoleErrors = errors;`, `});`, '', ); } for (const screen of screens) { const testName = sanitizeTestName(screen.name); lines.push( `test('${escapeString(testName)}', async ({ page }) => {`, ` await page.goto('${escapeString(screen.url)}');`, ` await expect(page).toHaveURL(/${escapeString(screen.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))}/);`, ); if (screen.title) { lines.push(` await expect(page).toHaveTitle(/${escapeString(screen.title.slice(0, 40).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))}/i);`); } if (screen.heading) { const h = escapeString(screen.heading.slice(0, 60)); if (needsSmartLocator && screen.smartLocator) { const tag = escapeString(screen.smartLocator.tag); const text = escapeString(screen.smartLocator.text ?? ''); lines.push( ` const headingEl = await smartLocator(page, 'h1, h2, h3', { tag: '${tag}', text: '${text}' });`, ` await expect(headingEl.first()).toContainText('${h}', { ignoreCase: true });`, ); } else { lines.push(` await expect(page.locator('h1, h2, h3').first()).toContainText('${h}', { ignoreCase: true });`); } } if (includeConsoleCheck) { lines.push(` expect((page as any).__consoleErrors ?? []).toHaveLength(0);`); } lines.push(`});`, ''); } return lines.join('\n'); } export function generatePlaywrightConfig(opts: { baseUrl?: string; reportDir?: string } = {}): string { const { baseUrl = 'http://localhost:3000', reportDir = 'playwright-report' } = opts; return `// playwright.config.ts — generated by ZeTa Crawler import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', reporter: [['html', { outputFolder: '${reportDir}' }]], use: { baseURL: '${baseUrl}', headless: true, screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { browserName: 'chromium' } }, { name: 'firefox', use: { browserName: 'firefox' } }, { name: 'webkit', use: { browserName: 'webkit' } }, ], }); `; }