import { expect, test } from '@playwright/test'; import { copyFile, mkdir, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; const demoMode = process.env.E2E_DEMO_MODE === '1'; const demoDelayMs = 2000; const demoImagePaths = [ path.join(process.cwd(), 'e2e', 'fixtures', 'menu-burger.jpg'), path.join(process.cwd(), 'e2e', 'fixtures', 'menu-drink.jpg'), path.join(process.cwd(), 'e2e', 'fixtures', 'menu-fries.jpg'), ]; const demoOutputDirectory = path.join(process.cwd(), 'test-results', 'e2e', 'kaggle-demo'); const demoRunId = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14); const prompt = 'Maak een professionele fastfood menu deal met burger, drinken en friet op een schone witte menu-achtergrond'; type GenerationMetrics = { durationSeconds?: number; inferenceSeconds?: number; modelLoadSeconds?: number; setupSeconds?: number; steps?: number; gpuName?: string; }; async function waitForDemo(): Promise { if (!demoMode) { return; } await new Promise((resolvePromise) => setTimeout(resolvePromise, demoDelayMs)); } test('generates a real menu deal image through Kaggle', async ({ page }) => { test.setTimeout(7_500_000); await page.goto('/'); await waitForDemo(); await expect(page.getByRole('heading', { name: 'Maak een menu-afbeelding.' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Maak nieuwe beelden' })).toBeDisabled(); await page.locator('input[type="file"]:not([webkitdirectory])').setInputFiles(demoImagePaths); await waitForDemo(); await expect(page.getByText('menu-burger.jpg')).toBeVisible(); await expect(page.getByText('menu-drink.jpg')).toBeVisible(); await expect(page.getByText('menu-fries.jpg')).toBeVisible(); await page.getByLabel('Type beeld').selectOption('menu'); await page.getByLabel('Stijl').selectOption('studio'); await page.getByLabel('Formaat').selectOption('1:1'); await page.getByLabel('Instructie').fill(prompt); await waitForDemo(); const generationResponsePromise = page.waitForResponse('http://127.0.0.1:3000/api/generations', { timeout: 7_200_000 }); await page.getByRole('button', { name: 'Maak nieuwe beelden' }).click(); await expect(page.getByRole('button', { name: 'Beelden maken...' })).toBeVisible(); await waitForDemo(); const generationResponse = await generationResponsePromise; const generationBody = await generationResponse.text(); if (generationResponse.status() !== 202) { await writeFailureReport(generationResponse.status(), generationBody); } expect(generationResponse.status(), generationBody).toBe(202); const jobId = parseGenerationJobId(generationBody); const completedGenerationResponse = await page.waitForResponse(async (response) => { if (response.url() !== `http://127.0.0.1:3000/api/generations/${jobId}` || response.status() !== 200) { return false; } const body = await response.text(); return parseGenerationJobStatus(body) === 'completed'; }, { timeout: 7_200_000 }); const completedGenerationBody = await completedGenerationResponse.text(); const generationMetrics = parseGenerationMetrics(completedGenerationBody); await expect(page.getByRole('button', { name: 'Maak nieuwe beelden' })).toBeVisible(); await expect(page.getByText(/beeld is klaar voor controle\./).first()).toBeVisible({ timeout: 30_000 }); await waitForDemo(); await page.locator('.image-card').first().getByRole('button', { name: 'Clean studio' }).click(); await expect(page.locator('.image-card').first().getByText(/beeld is klaar voor controle\./)).toBeVisible(); await waitForDemo(); const generatedSource = await page.locator('.image-card').first().locator('.generated img').getAttribute('src'); expect(generatedSource?.startsWith('data:image/')).toBe(true); if (generatedSource !== null) { await writeDemoReport(generatedSource, generationMetrics); } await page.locator('.image-card').first().getByRole('button', { name: 'Verwijder' }).click(); await waitForDemo(); await expect(page.getByText('menu-burger.jpg')).not.toBeVisible(); }); async function writeDemoReport(generatedSource: string, metrics: GenerationMetrics | undefined): Promise { const originalFileNames = ['input-menu-burger.jpg', 'input-menu-drink.jpg', 'input-menu-fries.jpg']; const generatedFileName = `generated-${demoRunId}.png`; const reportFileName = 'result.md'; await resetDemoOutputDirectory(); await mkdir(demoOutputDirectory, { recursive: true }); for (const [index, imagePath] of demoImagePaths.entries()) { await copyFile(imagePath, path.join(demoOutputDirectory, originalFileNames[index])); } const generatedImage = decodeDataUrl(generatedSource); await writeFile(path.join(demoOutputDirectory, generatedFileName), generatedImage); await writeFile(path.join(demoOutputDirectory, 'generated.png'), generatedImage); await writeFile( path.join(demoOutputDirectory, reportFileName), [ '# Kaggle FLUX.2 klein Production Menu E2E Result', '', '## Prompt', '', prompt, '', '## Input Images', '', ...originalFileNames.map((fileName) => `![Input](./${fileName})`), '', '## Timing', '', ...buildTimingLines(metrics), '', '## Visual Acceptance Criteria', '', '- One composed menu-deal image, not a copied collage.', '- White menu-board/product photography background.', '- Burger, exactly one drink, and exactly one side item must be visible and correctly scaled.', '- No duplicate drinks, duplicate sides, or extra products.', '- No added brand logos, prices, labels, or promotional text.', '', '## Generated Image', '', `![Generated](./${generatedFileName})`, '', ].join('\n'), ); } function buildTimingLines(metrics: GenerationMetrics | undefined): string[] { if (metrics === undefined) { return ['- Model inference: not reported by provider.']; } return [ `- Model inference only: ${formatSeconds(metrics.inferenceSeconds)}`, `- Model load: ${formatSeconds(metrics.modelLoadSeconds)}`, `- Setup before model load: ${formatSeconds(metrics.setupSeconds)}`, `- Total Kaggle kernel runtime: ${formatSeconds(metrics.durationSeconds)}`, `- Steps: ${metrics.steps ?? 'unknown'}`, `- GPU: ${metrics.gpuName ?? 'unknown'}`, ]; } function formatSeconds(seconds: number | undefined): string { if (seconds === undefined) { return 'unknown'; } return `${seconds.toFixed(2)}s`; } function parseGenerationMetrics(responseBody: string): GenerationMetrics | undefined { const response = JSON.parse(responseBody); if (!hasMetrics(response)) { return undefined; } return response.result?.metrics ?? response.metrics; } function parseGenerationJobId(responseBody: string): string { const response = JSON.parse(responseBody); if (!hasJobId(response)) { throw new Error('Generation response did not include a job id'); } return response.jobId; } function parseGenerationJobStatus(responseBody: string): string | undefined { const response = JSON.parse(responseBody); if (typeof response !== 'object' || response === null || !('status' in response)) { return undefined; } const status = (<{ status?: unknown }>response).status; return typeof status === 'string' ? status : undefined; } function hasMetrics(value: unknown): value is { metrics: GenerationMetrics } { if (typeof value !== 'object' || value === null) { return false; } const response = <{ metrics?: unknown; result?: { metrics?: unknown } }>value; const metrics = response.result?.metrics ?? response.metrics; return typeof metrics === 'object' && metrics !== null; } function hasJobId(value: unknown): value is { jobId: string } { if (typeof value !== 'object' || value === null || !('jobId' in value)) { return false; } return typeof (<{ jobId?: unknown }>value).jobId === 'string'; } async function writeFailureReport(status: number, responseBody: string): Promise { const originalFileNames = ['input-menu-burger.jpg', 'input-menu-drink.jpg', 'input-menu-fries.jpg']; const reportFileName = 'result.md'; await resetDemoOutputDirectory(); await mkdir(demoOutputDirectory, { recursive: true }); for (const [index, imagePath] of demoImagePaths.entries()) { await copyFile(imagePath, path.join(demoOutputDirectory, originalFileNames[index])); } await writeFile( path.join(demoOutputDirectory, reportFileName), [ '# Kaggle FLUX.2 klein Production Menu E2E Result', '', '## Status', '', `Generation failed with HTTP ${status}.`, '', '## Prompt', '', prompt, '', '## Input Images', '', ...originalFileNames.map((fileName) => `![Input](./${fileName})`), '', '## Error', '', '```json', responseBody, '```', '', ].join('\n'), ); } async function resetDemoOutputDirectory(): Promise { await rm(demoOutputDirectory, { force: true, recursive: true }); } function decodeDataUrl(dataUrl: string): Buffer { const base64Marker = ';base64,'; const markerIndex = dataUrl.indexOf(base64Marker); if (markerIndex === -1) { throw new Error('Generated image was not a base64 data URL'); } return Buffer.from(dataUrl.slice(markerIndex + base64Marker.length), 'base64'); }