// Isolated server/data; no production account, report or paid API call. import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import http from 'node:http'; import zlib from 'node:zlib'; import { spawn, execSync } from 'node:child_process'; import { once } from 'node:events'; import { fileURLToPath, pathToFileURL } from 'node:url'; const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-login-perf-')); const child = spawn(process.execPath, ['server.js'], { cwd: fileURLToPath(new URL('../', import.meta.url)), env: { ...process.env, PORT: '18091', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot, SKIN_API_KEY: 'fixture', SKIN_API_SECRET: 'fixture', PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '' }, stdio: ['ignore', 'pipe', 'pipe'] }); const base = 'http://127.0.0.1:18091/'; let browser; async function raw(route, headers = {}) { return new Promise((resolve, reject) => { http.get(base + route, { headers }, response => { const chunks = []; response.on('data', data => chunks.push(data)); response.on('end', () => resolve({ status: response.statusCode, headers: response.headers, body: Buffer.concat(chunks) })); }).on('error', reject); }); } try { await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('test startup timeout')), 10000); child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timer); resolve(); } }); child.once('exit', code => { clearTimeout(timer); reject(new Error('server exited: ' + code)); }); }); for (const route of ['index.html', 'js/app.js', 'css/ui-20261009.css', 'vendor/faceapi/face-api.js']) { const plain = await raw(route); const compressed = await raw(route, { 'Accept-Encoding': 'gzip' }); assert.equal(compressed.status, 200); assert.equal(compressed.headers['content-encoding'], 'gzip'); assert.equal(compressed.headers.vary, 'Accept-Encoding'); assert.equal(compressed.headers['cache-control'], 'no-cache'); assert.deepEqual(zlib.gunzipSync(compressed.body), plain.body); assert(compressed.body.length < plain.body.length * .5, route); const cached = await raw(route, { 'Accept-Encoding': 'gzip', 'If-None-Match': compressed.headers.etag }); assert.equal(cached.status, 304); assert.equal(cached.body.length, 0); assert.equal((await raw(route, { 'Accept-Encoding': 'gzip;q=0' })).headers['content-encoding'], undefined); } const session = await raw('api/auth/session', { 'Accept-Encoding': 'gzip' }); assert.equal(session.headers['cache-control'], 'no-store'); assert.equal(session.headers.etag, undefined); assert.equal(session.headers['content-encoding'], undefined); assert.equal((await raw('data/accounts/identities.json')).status, 403); browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 375, height: 726 } }); const errors = [], requests = []; page.on('pageerror', error => errors.push(error.message)); page.on('request', request => requests.push(new URL(request.url()).pathname)); // If the old eager script is reintroduced, it will fail instead of silently // passing on a fast developer machine. Login must not depend on this request. await page.route('**/vendor/faceapi/face-api.js', route => route.abort()); const cdp = await page.context().newCDPSession(page); await cdp.send('Network.enable'); await cdp.send('Network.emulateNetworkConditions', { offline: false, latency: 150, downloadThroughput: 250000, uploadThroughput: 125000 }); await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); const start = Date.now(); await page.goto(base, { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查')); const readyMs = Date.now() - start; await page.waitForLoadState('networkidle'); assert(!requests.some(p => /faceapi|models|\.woff2$|bg-splash\.jpg$/.test(p)), JSON.stringify(requests)); const resourceBytes = await page.evaluate(() => performance.getEntriesByType('resource').reduce((sum, r) => sum + r.encodedBodySize, 0)); assert(resourceBytes < 350000, 'login resource budget exceeded: ' + resourceBytes); await page.click('[data-action="guest-experience"]'); await page.waitForSelector('#s-splash.active'); await page.waitForTimeout(400); // Allow the home entry animation to settle. const artifacts = process.env.H5_ARTIFACT_DIR; if (artifacts) { fs.mkdirSync(artifacts, { recursive: true }); await page.screenshot({ path: path.join(artifacts, 'guest-no-login.png') }); } await page.keyboard.press('Escape'); assert.equal(await page.locator('#p-login-success.show').count(), 0); assert.equal(await page.locator('#s-splash').evaluate(e => e.inert), false); for (const width of [320, 375, 390, 480]) { await page.setViewportSize({ width, height: 726 }); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); const badge = await page.locator('.account-status-home').boundingBox(); const slogan = await page.locator('#s-splash .splash-slogan').boundingBox(); assert(badge.y + badge.height <= slogan.y, 'account badge overlaps home slogan at ' + width); } await page.waitForLoadState('networkidle'); assert(!requests.some(p => /faceapi|models|\.woff2$/.test(p)), JSON.stringify(requests)); if (artifacts) await page.screenshot({ path: path.join(artifacts, 'browser-home-status.png') }); await page.click('[data-action="history"]'); await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('不保存')); assert.equal(await page.locator('#s-history [data-auth-status]').textContent(), '游客模式'); if (artifacts) await page.screenshot({ path: path.join(artifacts, 'browser-history-status.png') }); assert.equal(await page.locator('[data-action="switch-login"]').count(), 0); await page.click('#s-history [data-action="open-wechat-login"]'); assert.equal(await page.locator('#s-login.active').count(), 1); assert.deepEqual(errors, []); console.log('PASS: gzip/304/revalidation, no-store private API, login without fonts/face engine, success dialog/Escape/status/switching/layout; throttled cold login=' + readyMs + 'ms, resources=' + resourceBytes + ' bytes'); // Lazy engine loading must still support real models and retry after failure. await cdp.send('Network.emulateNetworkConditions', { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }); await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 }); await page.goBack(); await page.goBack(); await page.waitForSelector('#s-splash.active'); await page.evaluate(() => { navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('test denial', 'NotAllowedError')); }); await page.click('[data-action="open-privacy"]'); 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(400); } await page.click('[data-action="submit-profile"]'); await page.click('[data-action="start-camera"]'); await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('系统相机')); await page.waitForTimeout(300); assert.equal(requests.filter(p => p.endsWith('/face-api.js')).length, 1, 'engine must load only when entering camera'); await page.goBack(); await page.waitForSelector('#s-profile.active'); await page.unroute('**/vendor/faceapi/face-api.js'); await page.click('[data-action="submit-profile"]'); await page.click('[data-action="start-camera"]'); await page.waitForFunction(() => window.faceapi && faceapi.nets.tinyFaceDetector.isLoaded && faceapi.nets.faceLandmark68Net.isLoaded); assert.equal(requests.filter(p => p.endsWith('/face-api.js')).length, 2, 'failed lazy engine should be retried'); const sample = fs.readFileSync(fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/C3_180.jpg', import.meta.url))); const face = await page.evaluate(async data => { const image = await faceapi.fetchImage(data); const found = await faceapi.detectSingleFace(image, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: .3 })).withFaceLandmarks(); return found ? found.landmarks.positions.length : 0; }, 'data:image/jpeg;base64,' + sample.toString('base64')); assert.equal(face, 68, 'lazy-loaded engine must perform real landmark inference'); console.log('PASS: no engine at login; camera loads engine/models; failed script retries; real 68-point inference works'); // A stalled session request must leave a recoverable message, not spin forever // or discard the saved browser credential. Only this test shortens the timeout. const token = await page.evaluate(() => localStorage.getItem('juzi.identity.v1')); await page.addInitScript(() => { const original = window.setTimeout; window.setTimeout = (fn, ms, ...args) => original(fn, ms === 12000 ? 100 : ms, ...args); }); await page.route('**/api/auth/session', () => {}); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('网络响应超时')); assert.equal(await page.evaluate(() => localStorage.getItem('juzi.identity.v1')), token); assert.equal(await page.locator('#p-login-success.show').count(), 0); console.log('PASS: stalled session timeout is visible and preserves browser identity'); } finally { if (browser) await browser.close(); const stopped = once(child, 'exit'); child.kill(); await stopped; fs.rmSync(dataRoot, { recursive: true, force: true }); }