Files
juzi/h5/test/customer-feedback.mjs
T

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(); }