Files
behind_glass/scripts/ai-depth-smoke.mjs
T
2026-10-06 12:16:46 +09:00

128 lines
6.4 KiB
JavaScript

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/qa/dog1.jpeg');
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;