121 lines
8.4 KiB
JavaScript
121 lines
8.4 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-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');
|
|
await page.waitForFunction(() => { const i = document.querySelector('.scan-flip-icon img'); return i.complete && i.naturalWidth === 80 && i.naturalHeight === 80; });
|
|
assert.equal(await page.locator('.scan-flip-icon img').getAttribute('src'), await page.locator('.cam-flip img').getAttribute('src'), 'scan and camera share replacement icon');
|
|
assert.equal(await page.locator('.scan-flip-icon').evaluate(el => getComputedStyle(el).pointerEvents), 'none');
|
|
assert.equal(await page.locator('.scan-flip-icon [data-action], button.scan-flip-icon').count(), 0, 'scan flip stays decorative');
|
|
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();
|
|
const icon = await page.locator('.scan-flip-icon img').boundingBox();
|
|
assert(Math.abs(icon.width / art.width - 80 / 1080) < .001, 'icon scales with PSD stage');
|
|
assert(Math.abs(icon.height - icon.width) < 1, 'replacement remains square');
|
|
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(); }
|