146 lines
9.0 KiB
JavaScript
146 lines
9.0 KiB
JavaScript
// Explicit opt-in: one real Face++ call with the repository's three-view sample.
|
|
// Creates a separate browser identity/report; never alters existing user data.
|
|
// RUN_LIVE_ANALYZE=1 H5_BASE_URL=https://host/state/ node h5/test/production-loop.mjs
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { execSync } from 'node:child_process';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
assert.equal(process.env.RUN_LIVE_ANALYZE, '1', 'Requires explicit RUN_LIVE_ANALYZE=1 (paid upstream call)');
|
|
const base = process.env.H5_BASE_URL;
|
|
assert(base && base.endsWith('/'), 'Set H5_BASE_URL with trailing slash');
|
|
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
|
const samples = fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/', import.meta.url));
|
|
const artifacts = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-production-loop-'));
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(60000);
|
|
const errors = [];
|
|
page.on('pageerror', e => errors.push(e.message));
|
|
// Exercise the denied-camera fallback; hardware camera behavior needs a phone.
|
|
await context.addInitScript(() => {
|
|
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('test denial', 'NotAllowedError'));
|
|
window.__analyzeTimeouts = [];
|
|
const original = window.setTimeout;
|
|
window.setTimeout = function (fn, delay, ...args) {
|
|
if (delay >= 45000) window.__analyzeTimeouts.push(delay);
|
|
return original(fn, delay, ...args);
|
|
};
|
|
});
|
|
// Test model-unavailable fallback as well, without relying on machine vision.
|
|
await page.route('**/models/**', route => route.abort());
|
|
try {
|
|
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
|
await page.check('#login-consent');
|
|
await page.click('[data-action="login-browser"]');
|
|
await page.waitForSelector('#s-splash.active');
|
|
await page.click('[data-action="login-confirm"]');
|
|
const api = context.request;
|
|
assert.equal((await api.get(base + 'api/reports')).status(), 200);
|
|
const cookie = (await context.cookies()).find(c => c.httpOnly);
|
|
assert(cookie && cookie.secure && cookie.sameSite === 'Lax' && cookie.path === new URL(base).pathname);
|
|
assert.equal((await api.post(base + 'api/skin-analyze', { data: {} })).status(), 400);
|
|
assert.equal((await api.post(base + 'api/skin-analyze', { data: {}, headers: { Origin: 'https://invalid.example' } })).status(), 403);
|
|
console.log('PASS: public HTTPS, secure scoped cookie, missing-image validation, cross-origin denial');
|
|
|
|
await page.click('[data-action="open-privacy"]');
|
|
await page.click('[data-action="agree-continue"]');
|
|
assert.equal(await page.locator('#s-profile.active').count(), 0);
|
|
await page.click('#privacy-check');
|
|
await page.click('[data-action="agree-continue"]');
|
|
await page.click('[data-action="submit-profile"]');
|
|
assert.equal(await page.locator('#s-profile.active').count(), 1);
|
|
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(400);
|
|
}
|
|
await page.click('[data-action="submit-profile"]');
|
|
await page.click('[data-action="start-camera"]');
|
|
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('系统相机'));
|
|
// Bad file must keep us on the same step and allow selecting a valid photo.
|
|
await page.setInputFiles('#cam-file', { name: 'broken.jpg', mimeType: 'image/jpeg', buffer: Buffer.from('not an image') });
|
|
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('读取失败'));
|
|
assert((await page.locator('#cam-step').textContent()).includes('1/3'));
|
|
console.log('PASS: consent/profile guards, denied camera + missing model fallback, invalid-image recovery');
|
|
let analyzeCalls = 0;
|
|
page.on('request', req => { if (req.url() === base + 'api/skin-analyze') analyzeCalls++; });
|
|
const responsePromise = page.waitForResponse(res => res.url() === base + 'api/skin-analyze', { timeout: 165000 });
|
|
// Capture sequence front → right → left; app maps to API front/left/right fields.
|
|
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));
|
|
}
|
|
const response = await responsePromise;
|
|
assert.equal(response.status(), 200);
|
|
const result = await response.json();
|
|
assert.equal(result.ok, true, JSON.stringify({ code: result.code, message: result.message }));
|
|
assert.equal(result.report_saved, true);
|
|
assert.equal(analyzeCalls, 1);
|
|
assert((await page.evaluate(() => window.__analyzeTimeouts)).includes(150000));
|
|
await page.waitForSelector('#s-report.active');
|
|
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
|
|
const snap = (await (await api.get(base + 'api/reports/' + result.record_id)).json()).report;
|
|
assert.equal(snap.photos.filter(Boolean).length, 3);
|
|
assert.deepEqual(snap.presentation, result.presentation);
|
|
assert.deepEqual(snap.payload.result.face_maps, result.result.face_maps);
|
|
const images = await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src));
|
|
const copy = await page.locator('.ditem-para').allTextContents();
|
|
for (const card of await page.locator('.card-detail').all()) {
|
|
for (const tab of await card.locator('.dtab').all()) {
|
|
await tab.click();
|
|
assert.equal(await card.locator('.dpanel.active').count(), 1);
|
|
}
|
|
}
|
|
await page.locator('.ditem-img:visible').first().click();
|
|
await page.waitForSelector('#p-zoom.show');
|
|
await page.goBack();
|
|
await page.waitForSelector('#p-zoom.show', { state: 'hidden' });
|
|
assert.equal(await page.locator('#s-report.active').count(), 1);
|
|
await page.screenshot({ path: path.join(artifacts, 'real-report.png'), fullPage: true });
|
|
console.log('PASS: three sample views → REAL Face++ → scoring → seven images/tabs → zoom/back → backend snapshot');
|
|
|
|
// Cookie loss is restored using this browser's stored random identity, not a new account.
|
|
await context.clearCookies();
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('#s-splash.active');
|
|
await page.click('[data-action="history"]');
|
|
await page.click('[data-report-id="' + result.record_id + '"]');
|
|
await page.waitForSelector('#s-report.active');
|
|
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
|
|
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src)), images);
|
|
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copy);
|
|
await page.goBack();
|
|
await page.waitForSelector('#s-history.active');
|
|
// Read failure + explicit retry must recover without losing the report.
|
|
await page.route('**/api/reports', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ ok: false, message: '测试临时不可用' }) }));
|
|
await page.click('[data-action="history-retry"]');
|
|
await page.waitForFunction(() => document.querySelector('#history-list').textContent.includes('测试临时不可用'));
|
|
await page.unroute('**/api/reports');
|
|
await page.click('[data-action="history-retry"]');
|
|
await page.waitForSelector('[data-report-id="' + result.record_id + '"]');
|
|
await page.screenshot({ path: path.join(artifacts, 'real-history.png') });
|
|
await page.goBack();
|
|
await page.waitForSelector('#s-splash.active');
|
|
console.log('PASS: cookie restoration, report reload with identical seven images/copy, history error/retry, browser back');
|
|
|
|
const other = await browser.newContext();
|
|
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
|
|
await other.request.post(base + 'api/auth/browser', { data: { consent: true } });
|
|
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 404);
|
|
for (const route of ['api/admin/stats', 'admin.html', 'data/accounts/identities.json', '%2fserver.js', 'lib/accounts.js', 'test/accounts.js']) {
|
|
assert([401,403,404].includes((await other.request.get(base + route)).status()), route);
|
|
}
|
|
await other.close();
|
|
assert.deepEqual(errors, []);
|
|
fs.writeFileSync(path.join(artifacts, 'summary.json'), JSON.stringify({ record_id: result.record_id, analyzeCalls, errors, tested_at: new Date().toISOString() }, null, 2));
|
|
console.log('PASS: cross-account isolation, admin/source/data protection, zero JS errors');
|
|
console.log('Test report (dedicated identity): ' + result.record_id + '; artifacts: ' + artifacts);
|
|
} finally {
|
|
await browser.close();
|
|
}
|