Files
juzi/h5/test/scan-photo.mjs
T

111 lines
7.8 KiB
JavaScript

// 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-photo-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');
await page.waitForFunction(() => { const img = document.querySelector('#scan-photo'); return img.complete && img.naturalWidth > 0; });
assert(pending);
const sent = pending.request().postDataJSON();
assert.equal(await page.locator('#scan-photo').getAttribute('src'), sent.front_image, 'waiting face is the current FRONT photo, not the last side or a placeholder');
assert.notEqual(sent.front_image, sent.left_image); assert.notEqual(sent.front_image, sent.right_image);
assert.equal(await page.locator('#scan-photo.mirror').count(), 0, 'uploaded/system camera photos must not be mirrored again');
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).objectFit), 'cover');
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(17, 17, 17)', 'loading/error does not expose white placeholder');
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').getAttribute('src'), null, 'face reference cleared on exit');
assert.equal(await page.locator('#p-analyzing.scan-running').count(), 0);
};
await profile();
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'no face before capture');
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');
for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) {
await page.setViewportSize({ width, height });
const photo = await page.locator('#scan-photo').boundingBox();
assert(Math.abs(photo.width - Math.min(width, 480)) < 1 && Math.abs(photo.height - height) < 1, 'same cover viewport as camera');
assert(Math.abs(photo.x - (width - photo.width) / 2) < 1 && Math.abs(photo.y) < 1, 'photo crop stays centered independently of PSD contain mask');
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-photo.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 the real photograph + status + cancel.
artworkFailure = true;
await profile(); await capture();
assert.equal(await page.locator('#p-analyzing.scan-artwork-ready').count(), 0);
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage), 'none');
assert.equal(await page.locator('.scan-status').evaluate(el => getComputedStyle(el).clipPath), 'none');
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).visibility), 'visible');
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 shows current front JPEG; original orientation/cover/PSD mask/scan; six viewport sizes; photo cleanup after cancel/error/success; stale response; one real reduced-motion cycle; image/mask failure fallback; no JS errors/backend writes');
} finally { await browser.close(); }