import { mkdir, writeFile } from 'node:fs/promises'; import { chromium } from 'playwright-core'; const baseUrl = process.env.AI_DEPTH_BASE_URL || 'http://127.0.0.1:5174/'; const outputDir = process.env.AI_DEPTH_OUTPUT_DIR || 'test-results/ai-depth'; const executablePath = process.env.CHROME_EXECUTABLE_PATH || process.env.PLAYWRIGHT_CHROMIUM_PATH; const launchOptions = { headless: true, }; if (executablePath) launchOptions.executablePath = executablePath; await mkdir(outputDir, { recursive: true }); const browser = await chromium.launch(launchOptions); const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 }); const errors = []; page.on('pageerror', (error) => errors.push({ type: 'pageerror', message: error.message })); page.on('console', (message) => { if (message.type() === 'error') errors.push({ type: 'console', message: message.text() }); }); page.on('requestfailed', (request) => errors.push({ type: 'requestfailed', url: request.url(), message: request.failure()?.errorText ?? '' })); await page.addInitScript(() => { const workerResults = []; Object.defineProperty(window, '__aiDepthWorkerResults', { value: workerResults }); const NativeWorker = window.Worker; window.Worker = new Proxy(NativeWorker, { construct(target, args) { const worker = new target(...args); worker.addEventListener('message', (event) => { const result = event.data; if (result?.type !== 'result' || !(result.values instanceof ArrayBuffer)) return; const values = new Uint8Array(result.values); const landmarks = { nose: [235, 310], leftEye: [198, 278], rightEye: [249, 273], face: [221, 267], leftEar: [158, 213], rightEar: [262, 206], body: [139, 419], }; const samples = Object.fromEntries(Object.entries(landmarks).map(([name, [x, y]]) => { const sx = Math.min(result.width - 1, Math.max(0, Math.round(x * result.width / 415))); const sy = Math.min(result.height - 1, Math.max(0, Math.round(y * result.height / 739))); return [name, { x: sx, y: sy, depth: values[sy * result.width + sx] / 255 }]; })); workerResults.push({ width: result.width, height: result.height, samples }); }); return worker; }, }); }); const report = { baseUrl, screenshots: {}, errors }; try { await page.goto(baseUrl, { waitUntil: 'domcontentloaded' }); await page.locator('#sample').click(); await page.waitForFunction(() => document.querySelector('#depth-provenance')?.textContent?.startsWith('수작업 테스트 Depth'), null, { timeout: 20000 }); await page.locator('#ai-depth').click(); await page.waitForFunction(() => { const provenance = document.querySelector('#depth-provenance')?.textContent ?? ''; const status = document.querySelector('#status')?.textContent ?? ''; return provenance.startsWith('AI 추정') || status.includes('AI 깊이 생성 실패'); }, null, { timeout: Number(process.env.AI_DEPTH_TIMEOUT_MS || 240000) }); const status = await page.locator('#status').textContent(); const provenance = await page.locator('#depth-provenance').textContent(); if (!provenance?.startsWith('AI 추정')) throw new Error(`AI inference did not complete: ${status}`); await page.screenshot({ path: `${outputDir}/no-glass.png`, fullPage: true }); report.screenshots.noGlass = `${outputDir}/no-glass.png`; report.statusAfterInference = status; report.provenance = provenance; report.depthResults = await page.evaluate(() => window.__aiDepthWorkerResults); const stage = await page.locator('#canvas-wrap').boundingBox(); if (!stage) throw new Error('Canvas stage is unavailable for drag QA.'); await page.mouse.move(stage.x + stage.width * 0.5, stage.y + stage.height * 0.5); await page.mouse.down(); await page.mouse.move(stage.x + stage.width * 0.62, stage.y + stage.height * 0.48, { steps: 5 }); await page.mouse.up(); await page.waitForTimeout(250); await page.screenshot({ path: `${outputDir}/no-glass-drag.png`, fullPage: true }); report.screenshots.noGlassDrag = `${outputDir}/no-glass-drag.png`; await page.locator('#view-mode').selectOption('final'); await page.waitForTimeout(300); await page.screenshot({ path: `${outputDir}/final-drag.png`, fullPage: true }); report.screenshots.finalAfterDrag = `${outputDir}/final-drag.png`; await page.setViewportSize({ width: 390, height: 844 }); await page.waitForTimeout(300); report.mobileHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth); if (report.mobileHorizontalOverflow) throw new Error('The page has horizontal overflow at 390px.'); await page.screenshot({ path: `${outputDir}/final-mobile.png`, fullPage: true }); report.screenshots.finalMobile = `${outputDir}/final-mobile.png`; await page.setViewportSize({ width: 1280, height: 900 }); await page.locator('#view-mode').selectOption('noglass'); await page.locator('#ai-depth').click(); await page.waitForFunction(() => document.querySelector('#ai-depth')?.textContent?.includes('생성 중'), null, { timeout: 5000 }); await page.locator('#image').setInputFiles('public/assets/samples/dog1.jpg'); await page.waitForFunction(() => { const button = document.querySelector('#ai-depth'); const provenance = document.querySelector('#depth-provenance'); return button && !button.disabled && provenance?.hidden; }, null, { timeout: 20000 }); await page.waitForTimeout(1000); report.photoSwitchDuringAI = { passed: await page.locator('#depth-provenance').isHidden(), buttonEnabled: await page.locator('#ai-depth').isEnabled(), }; if (!report.photoSwitchDuringAI.passed || !report.photoSwitchDuringAI.buttonEnabled) { throw new Error('AI result was applied after the active photo changed.'); } } catch (error) { report.failure = error instanceof Error ? error.message : String(error); report.status = await page.locator('#status').textContent().catch(() => ''); report.provenance = await page.locator('#depth-provenance').textContent().catch(() => ''); report.depthResults = await page.evaluate(() => window.__aiDepthWorkerResults).catch(() => []); } finally { report.errors = errors; await writeFile(`${outputDir}/report.json`, JSON.stringify(report, null, 2)); await browser.close(); } console.log(JSON.stringify(report, null, 2)); if (report.failure) process.exitCode = 1;