111 lines
7.6 KiB
JavaScript
111 lines
7.6 KiB
JavaScript
// Read-only mock page. No account creation, report writes or Face++ call.
|
|
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 browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
|
const errors = [];
|
|
page.on('pageerror', e => errors.push(e.message));
|
|
await page.goto(base + '?mock=1&screen=report');
|
|
await page.waitForFunction(() => document.querySelector('#stat-score b').textContent !== '--' && [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
|
const cdp = await page.context().newCDPSession(page);
|
|
async function swipe(card, dx, dy = 0) {
|
|
const frame = card.locator('.dpanel.active .viz-frame');
|
|
await frame.scrollIntoViewIfNeeded();
|
|
const box = await frame.boundingBox(), x = box.x + box.width / 2, y = Math.min(500, box.y + box.height / 2);
|
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y }] });
|
|
for (let i = 1; i <= 8; i++) {
|
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: x + dx * i / 8, y: y + dy * i / 8 }] });
|
|
}
|
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
|
await page.waitForTimeout(300);
|
|
}
|
|
const cards = page.locator('.card-detail');
|
|
await swipe(cards.nth(0), -85);
|
|
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '1');
|
|
assert.equal(await page.locator('#p-zoom.show').count(), 0);
|
|
await swipe(cards.nth(0), 85);
|
|
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0');
|
|
await swipe(cards.nth(0), 0, -85);
|
|
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0', 'vertical scrolling must not change tab');
|
|
await swipe(cards.nth(1), -85);
|
|
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '1');
|
|
await swipe(cards.nth(1), -85);
|
|
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '2');
|
|
await swipe(cards.nth(1), 85);
|
|
await cards.nth(1).locator('.dtab').first().click();
|
|
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '0');
|
|
await cards.nth(1).locator('.dtab').first().focus();
|
|
await page.keyboard.press('ArrowRight');
|
|
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '1');
|
|
await swipe(cards.nth(2), -85);
|
|
assert.equal(await cards.nth(2).locator('.dtab.active').getAttribute('data-tab'), '1');
|
|
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0');
|
|
const image = cards.nth(0).locator('.dpanel.active .ditem-img');
|
|
await image.click();
|
|
await page.waitForSelector('#p-zoom.show');
|
|
assert.equal(await page.locator('#zoom-img').getAttribute('src'), await image.getAttribute('src'));
|
|
await page.click('#p-zoom');
|
|
await page.waitForSelector('#p-zoom.show', { state: 'hidden' });
|
|
await page.waitForTimeout(300);
|
|
assert((await page.locator('#ditem-para2').textContent()).includes('抬头纹(额头纹)'));
|
|
assert((await page.locator('#ditem-para2').textContent()).includes('右法令纹'));
|
|
assert((await page.locator('#smooth-map-note').textContent()).includes('毛孔'));
|
|
const focused = await page.locator('.ditem-img').evaluateAll(images => images.map(i => ({ focused: i.dataset.crop, ratio: i.naturalWidth / i.naturalHeight })));
|
|
assert(focused.every(i => i.focused === 'face' && Math.abs(i.ratio - 215 / 278) < .005));
|
|
const fontSizes = [];
|
|
for (const width of [320, 375, 390, 414, 480]) {
|
|
await page.setViewportSize({ width, height: 844 });
|
|
await page.waitForTimeout(150);
|
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'page overflow ' + width);
|
|
fontSizes.push(await page.locator('#tx-tone').evaluate(e => getComputedStyle(e).fontSize));
|
|
assert.equal(await page.locator('#product-name .product-line').count(), 2);
|
|
for (const line of await page.locator('#product-name .product-line').all()) {
|
|
assert.equal(await line.evaluate(e => getComputedStyle(e).whiteSpace), 'nowrap');
|
|
assert(await line.evaluate(e => e.scrollWidth <= e.clientWidth), 'product line overflow ' + width);
|
|
}
|
|
}
|
|
assert(fontSizes.every(size => size === '12px'), JSON.stringify(fontSizes));
|
|
assert.equal(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--yellow').trim()), '#F5F078');
|
|
if (process.env.H5_ARTIFACT_DIR) {
|
|
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
|
await page.setViewportSize({ width: 375, height: 726 });
|
|
await image.scrollIntoViewIfNeeded();
|
|
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '01-face-focused.png'), animations: 'disabled' });
|
|
await cards.nth(0).locator('.dtab').nth(1).click();
|
|
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '02-wrinkle-regions.png'), animations: 'disabled' });
|
|
await cards.nth(1).locator('.dtab').nth(1).click();
|
|
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '03-smooth-maps.png'), animations: 'disabled' });
|
|
await page.locator('.card-product').scrollIntoViewIfNeeded();
|
|
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '04-product-lines.png'), animations: 'disabled' });
|
|
}
|
|
// Change only the in-browser fixture. Exercise the actual renderer and age-only
|
|
// recommendation outside boundary windows, without production report writes.
|
|
for (const [age, name, tier] of [[40, '以我自在有势挺括', 'II'], [60, '以我从容有态构塑', 'III']]) {
|
|
await page.route('**/assets/mock/report-fixture.js', async route => {
|
|
const response = await route.fetch();
|
|
await route.fulfill({ response, body: await response.text() + '\nMOCK_FIXTURE.result.skin_age = { value: ' + age + ' };\ndelete MOCK_FIXTURE.result.face_maps.texture_enhanced_blackheads;' });
|
|
});
|
|
await page.goto(base + '?mock=1&screen=report');
|
|
await page.waitForFunction(() => document.querySelector('#product-name .product-line'));
|
|
assert.equal(await page.locator('#product-title').textContent(), "1'me 主场精华 " + tier);
|
|
assert.equal(await page.locator('#product-name .product-line').first().textContent(), name);
|
|
assert(!(await page.locator('#smooth-map-note').textContent()).includes('红色'), 'missing blackhead map must not be claimed');
|
|
for (const width of [320, 375, 390, 414, 480]) {
|
|
await page.setViewportSize({ width, height: 844 });
|
|
for (const line of await page.locator('#product-name .product-line').all()) {
|
|
assert(await line.evaluate(e => e.scrollWidth <= e.clientWidth), 'tier ' + tier + ' product line overflow ' + width);
|
|
}
|
|
assert(await page.locator('#product-title').evaluate(e => e.scrollWidth <= e.clientWidth), 'tier ' + tier + ' title overflow ' + width);
|
|
}
|
|
await page.unroute('**/assets/mock/report-fixture.js');
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
console.log('PASS: real mobile swipes in three independent groups, vertical scroll vs swipe, click/keyboard tabs, no zoom after swipe, zoom still works, seven face-focused maps, 12-region copy, fixed fonts/all three product lines in five widths, partial map caption, PSD yellow, no JS errors');
|
|
} finally { await browser.close(); }
|