130 lines
9.4 KiB
JavaScript
130 lines
9.4 KiB
JavaScript
// Browser-local session/preview fixtures. No account/report writes or paid calls.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { execSync } from 'node:child_process';
|
|
import { 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 artifacts = process.env.H5_ARTIFACT_DIR;
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
|
const errors = [], requests = [];
|
|
page.on('pageerror', e => errors.push(e.message));
|
|
page.on('request', r => requests.push(new URL(r.url()).pathname));
|
|
await page.route('**/api/auth/session', route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, user: { id: 'second-batch-fixture', kind: 'browser' } }) }));
|
|
await page.goto(base);
|
|
await page.waitForSelector('#s-splash.active');
|
|
await page.waitForLoadState('networkidle');
|
|
assert(!requests.some(p => /\.woff2$|faceapi/.test(p)), 'entry must not download complete fonts or face engine');
|
|
for (const width of [320, 375, 390, 414, 480]) {
|
|
await page.setViewportSize({ width, height: 726 });
|
|
const home = await page.evaluate(() => {
|
|
const en = getComputedStyle(document.querySelector('.splash-slogan-en'));
|
|
const zh = getComputedStyle(document.querySelector('.splash-slogan-zh'));
|
|
const text = document.querySelector('#s-splash .brand-text');
|
|
const style = getComputedStyle(text);
|
|
const logo = document.querySelector('#s-splash .brand-logo').getBoundingClientRect();
|
|
const row = document.querySelector('#s-splash .brand-row').getBoundingClientRect();
|
|
const slogan = document.querySelector('.splash-slogan').getBoundingClientRect();
|
|
const badge = document.querySelector('.account-status-home').getBoundingClientRect();
|
|
const ctx = document.createElement('canvas').getContext('2d');
|
|
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
const metrics = ctx.measureText(text.textContent);
|
|
return { en: en.fontWeight, zh: zh.fontWeight, size: style.fontSize, weight: style.fontWeight,
|
|
logoHeight: logo.height, inkHeight: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
|
|
logoRatio: logo.width / logo.height, rowX: row.x, rowRight: row.right,
|
|
badgeBottom: badge.bottom, sloganTop: slogan.top,
|
|
sloganLines: slogan.height / parseFloat(zh.lineHeight), overflow: document.documentElement.scrollWidth > innerWidth };
|
|
});
|
|
assert.equal(home.en, '400'); assert.equal(home.zh, '700');
|
|
assert.equal(home.size, '24px'); assert.equal(home.weight, '700');
|
|
assert.equal(home.logoHeight, 26);
|
|
assert(Math.abs(home.inkHeight - home.logoHeight) <= 4, 'logo/text optical height mismatch: ' + JSON.stringify(home));
|
|
assert(Math.abs(home.logoRatio - 132 / 59) < .01, 'logo must retain original aspect ratio');
|
|
assert(home.rowX >= 0 && home.rowRight <= width && !home.overflow, 'brand row overflows at ' + width);
|
|
assert(home.badgeBottom <= home.sloganTop, 'status badge overlaps slogan at ' + width);
|
|
assert(Math.abs(home.sloganLines - 3) < .1, 'slogan must retain three lines at ' + width);
|
|
}
|
|
await page.setViewportSize({ width: 375, height: 726 });
|
|
if (artifacts) {
|
|
fs.mkdirSync(artifacts, { recursive: true });
|
|
await page.screenshot({ path: path.join(artifacts, '00-home-clarified.png'), animations: 'disabled' });
|
|
}
|
|
await page.waitForLoadState('networkidle');
|
|
assert(!requests.some(p => /\.woff2$|faceapi/.test(p)), 'home typography must not add full fonts or face engine');
|
|
await page.click('[data-action="open-privacy"]');
|
|
await page.waitForSelector('link[href="css/fonts.css"]', { state: 'attached' });
|
|
await page.evaluate(async () => { await document.fonts.load('700 23px "MiSans Demibold"', "欢迎体验肌肤状态评估服务"); });
|
|
await page.waitForLoadState('networkidle');
|
|
assert(requests.some(p => /MiSans-Demibold\.woff2$/.test(p)), 'consent must load PSD Demibold before profile');
|
|
assert.equal(await page.locator('#p-privacy .popup-title').evaluate(e => getComputedStyle(e).fontWeight), '700');
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
|
|
async function actualFont(selector, expected) {
|
|
const { root } = await cdp.send('DOM.getDocument');
|
|
const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: root.nodeId, selector });
|
|
const { fonts } = await cdp.send('CSS.getPlatformFontsForNode', { nodeId });
|
|
assert(fonts.some(f => f.isCustomFont && new RegExp(expected, 'i').test(f.postScriptName || f.familyName)), selector + ': ' + JSON.stringify(fonts));
|
|
}
|
|
await actualFont('#p-privacy .popup-title', 'Demibold');
|
|
if (artifacts) {
|
|
fs.mkdirSync(artifacts, { recursive: true });
|
|
await page.screenshot({ path: path.join(artifacts, '01-consent-demibold.png'), animations: 'disabled' });
|
|
}
|
|
await page.click('[data-action="close-privacy"]');
|
|
// Synthetic local stream only, to inspect full-bleed framing without camera permission.
|
|
await page.evaluate(async () => {
|
|
document.querySelectorAll('.screen').forEach(s => s.classList.toggle('active', s.id === 's-cam'));
|
|
document.querySelector('#s-cam').classList.add('cam-live');
|
|
const canvas = document.createElement('canvas'); canvas.width = 375; canvas.height = 726;
|
|
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#587581'; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
const video = document.querySelector('#cam-video'); video.srcObject = canvas.captureStream(5); await video.play();
|
|
});
|
|
await page.waitForTimeout(400); // Let the existing screen-entry translate animation finish.
|
|
await page.evaluate(async () => { await document.fonts.load('700 23px "MiSans Demibold"', '请正脸出镜'); });
|
|
await actualFont('#cam-step', 'Demibold');
|
|
for (const [step, label] of [['0', '1/3<br>请正脸出镜'], ['2', '2/3<br>请右脸出镜'], ['1', '3/3<br>请左脸出镜']]) {
|
|
await page.evaluate(({ step, label }) => { document.querySelector('#cam-mask').dataset.step = step; document.querySelector('#cam-step').innerHTML = label; }, { step, label });
|
|
for (const width of [320, 375, 390, 414, 480]) {
|
|
await page.setViewportSize({ width, height: 726 });
|
|
const box = await page.locator('#cam-video').boundingBox();
|
|
assert(Math.abs(box.width - width) < 1 && Math.abs(box.height - 726) < 1, 'preview must fill viewport');
|
|
assert(Math.abs(box.x) < 1 && Math.abs(box.y) < 1);
|
|
assert.equal(await page.locator('#cam-video').evaluate(e => getComputedStyle(e).borderWidth), '0px');
|
|
for (const state of ['', 'no-face', 'ok']) {
|
|
await page.locator('#cam-mask').evaluate((e, state) => { e.classList.remove('no-face', 'ok'); if (state) e.classList.add(state); }, state);
|
|
assert.equal(await page.locator('.cam-line').evaluate(e => getComputedStyle(e).display), 'none');
|
|
}
|
|
assert(await page.locator('#cam-cut path[data-shape="' + step + '"]').evaluate(e => e.getAttribute('d').includes('Q')));
|
|
assert.equal(await page.locator('#cam-step').evaluate(e => getComputedStyle(e).fontSize), '21.5px');
|
|
assert.equal(await page.locator('.cam-feature-lines[data-shape="' + step + '"]').evaluate(e => getComputedStyle(e).display), 'block');
|
|
}
|
|
if (artifacts) {
|
|
await page.setViewportSize({ width: 375, height: 726 });
|
|
await page.screenshot({ path: path.join(artifacts, '02-camera-' + step + '.png'), animations: 'disabled' });
|
|
}
|
|
}
|
|
await page.goto(base + '?mock=1&screen=report');
|
|
await page.waitForFunction(() => document.querySelector('#stat-score b').textContent !== '--');
|
|
await page.evaluate(async () => { await document.fonts.load('700 40px "MiSans Demibold"', '0123456789'); await document.fonts.load('600 23px MiSans', '护理建议您的肌肤状态表现紧塑力'); });
|
|
await page.waitForLoadState('networkidle');
|
|
for (const selector of ['#stat-score b', '#stat-age b', '.dim-score b']) await actualFont(selector, 'Demibold');
|
|
for (const selector of ['#s-report .report-title', '.dimension-title', '.card-heading']) await actualFont(selector, 'Semibold');
|
|
for (const width of [320, 375, 390, 414, 480]) {
|
|
await page.setViewportSize({ width, height: 844 });
|
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
assert.equal(await page.locator('.card-heading').first().evaluate(e => getComputedStyle(e).fontSize), '23px');
|
|
}
|
|
if (artifacts) {
|
|
await page.setViewportSize({ width: 375, height: 726 });
|
|
await page.locator('#s-report').evaluate(e => { e.scrollTop = 0; });
|
|
await page.screenshot({ path: path.join(artifacts, '03-report-demibold-numbers.png'), animations: 'disabled' });
|
|
await page.locator('.card-care').scrollIntoViewIfNeeded();
|
|
await page.screenshot({ path: path.join(artifacts, '04-care-semibold.png'), animations: 'disabled' });
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
console.log('PASS: heavier Chinese slogan, matched logo/text optical height, three lines/no overlap in five widths, no fonts/engine at entry; early consent Demibold; actual PSD font glyphs for headings/numbers; full-bleed camera, no outline in any state, three rounded contours/retained guides; fixed fonts/no overflow in five widths; no JS errors');
|
|
} finally { await browser.close(); }
|