// Actual three-photo UI with in-memory auth/analysis. No backend writes/paid API. import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/'; const samples = fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/', import.meta.url)); const browser = await chromium.launch({ headless: true }); const errors = [], unexpectedWrites = []; try { const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true }); page.setDefaultTimeout(20000); page.on('pageerror', e => errors.push(e.message)); let pending, artworkFailure = false; await page.route('**/*', route => { const req = route.request(); if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'scan-placeholder-fixture', kind: 'browser' } } }); if (req.url().endsWith('/api/skin-analyze')) { pending = route; return; } if (req.method() !== 'GET') { unexpectedWrites.push(req.url()); return route.abort(); } if (/\.woff2$|\/vendor\/faceapi\//.test(req.url())) return route.abort(); if (artworkFailure && /scan-base\.webp|scan-face-mask\.png/.test(req.url())) return route.abort(); return route.continue(); }); await page.addInitScript(() => { navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('fixture denial', 'NotAllowedError')); window.__scanIterations = []; document.addEventListener('animationiteration', e => { if (e.target.matches('.scan-beam,.scan-status')) window.__scanIterations.push({ target: e.target.className, elapsed: e.elapsedTime }); }); }); async function profile(reload = true) { if (reload) await page.goto(base, { waitUntil: 'domcontentloaded' }); await page.click('[data-action="open-privacy"]'); if (!(await page.locator('#privacy-check').evaluate(el => el.classList.contains('checked')))) await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]'); for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) { await page.click(selector); await page.click('.ms-mobile-select.ms-show .ms-ensure'); await page.waitForTimeout(250); } } async function capture() { pending = null; await page.click('[data-action="submit-profile"]'); await page.click('[data-action="start-camera"]'); for (const [i, name] of ['C3_180.jpg', 'C3_135.jpg', 'C3_-135.jpg'].entries()) { await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i + 1); await page.waitForTimeout(1600); await page.setInputFiles('#cam-file', path.join(samples, name)); } await page.waitForSelector('#p-analyzing.show'); assert(pending); const sent = pending.request().postDataJSON(); assert.equal(await page.locator('#scan-photo').count(), 0, 'waiting never inserts a user face'); assert.equal(await page.locator('#p-analyzing img[src^="data:"]').count(), 0); assert.notEqual(sent.front_image, sent.left_image); assert.notEqual(sent.front_image, sent.right_image); return sent; } function rejectPending(message) { const route = pending; pending = null; return route.fulfill({ json: { ok: false, message } }); } const cleared = async () => { assert.equal(await page.locator('#scan-photo').count(), 0); assert.equal(await page.locator('#p-analyzing.scan-running').count(), 0); }; await profile(); assert.equal(await page.locator('#scan-photo').count(), 0, 'no user photo element'); await capture(); await page.waitForSelector('#p-analyzing.scan-artwork-ready'); assert((await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage)).includes('scan-face-mask.png')); assert.equal(await page.locator('.scan-beam').evaluate(el => getComputedStyle(el).animationName), 'report-scan'); const pixels = await page.locator('#scan-artwork').evaluate(image => { const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; const context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return [[540, 720], [540, 1200]].map(([x, y]) => [...context.getImageData(x, y, 1, 1).data]); }); assert(pixels.every(pixel => pixel.slice(0, 3).every(value => value > 245)), 'original white head is preserved'); for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) { await page.setViewportSize({ width, height }); const art = await page.locator('#scan-artwork').boundingBox(); assert(art.width <= width + 1 && art.height <= height + 1); assert(Math.abs(art.width / art.height - 1080 / 2091) < .001, 'PSD is contained without cropping'); assert(Math.abs(art.x - (width - art.width) / 2) < 1 && Math.abs(art.y - (height - art.height) / 2) < 1); assert(await page.locator('.scan-cancel').isVisible()); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); } await page.setViewportSize({ width: 375, height: 726 }); if (process.env.H5_ARTIFACT_DIR) { fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true }); await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'scan-placeholder.png') }); } await page.mouse.click(187, 350); assert(await page.locator('#p-analyzing').isVisible(), 'clicking the face does not cancel'); await page.click('[data-action="cancel-analysis"]'); await page.waitForSelector('#s-profile.active'); await cleared(); await rejectPending('迟到的分析不应影响页面'); await page.waitForTimeout(200); assert(!(await page.locator('#toast').textContent()).includes('迟到')); await capture(); await rejectPending('fixture analysis failure'); await page.waitForSelector('#s-profile.active'); await cleared(); // Fast success + reduced motion still completes one actual visible cycle. await page.emulateMedia({ reducedMotion: 'reduce' }); await capture(); await page.evaluate(() => { window.__scanIterations = []; }); const success = await page.evaluate(() => ({ ok: true, result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE, report_saved: true })); await pending.fulfill({ json: success }); pending = null; await page.waitForSelector('#s-report.active'); await cleared(); assert(await page.evaluate(() => window.__scanIterations.some(row => row.target === 'scan-beam' && row.elapsed >= 1)), 'fast success completes an actual cycle'); // Failed artwork/mask preserves a readable status and cancel, not a user photo. artworkFailure = true; await profile(); await capture(); assert.equal(await page.locator('#p-analyzing.scan-artwork-ready').count(), 0); assert.equal(await page.locator('.scan-status').evaluate(el => getComputedStyle(el).clipPath), 'none'); assert.equal(await page.locator('#scan-artwork').evaluate(img => getComputedStyle(img).visibility), 'hidden'); await page.click('[data-action="cancel-analysis"]'); await cleared(); await rejectPending('canceled'); assert.deepEqual(errors, []); assert.deepEqual(unexpectedWrites, []); console.log('PASS: actual three-photo waiting restores original white PSD head, no user photograph; six viewport sizes; cancel/error/success cleanup; stale response; one real reduced-motion cycle; image/mask failure fallback; no JS errors/backend writes'); } finally { await browser.close(); }