/** * 移动端逐屏截图(对照 prototypes/fmode-mobile-20261102/shots) * * 用法:node scripts/shots-mobile.mjs * 前置:npm run build:mock(MockApi 数据,无需真实登录) */ import { chromium, devices } from 'playwright'; import { mkdir } from 'node:fs/promises'; import { createServer } from 'node:http'; import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { extname, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const distDir = resolve(here, '../dist/fmode-app-v2/browser'); const outDir = resolve(here, '../test-results/mobile-shots'); const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.ico': 'image/x-icon', '.png': 'image/png', }; const server = createServer(async (req, res) => { try { const url = decodeURIComponent((req.url ?? '/').split('?')[0]); let file = join(distDir, url); if (!existsSync(file) || url === '/') file = join(distDir, 'index.html'); const body = await readFile(file); res.writeHead(200, { 'Content-Type': MIME[extname(file)] ?? 'application/octet-stream' }); res.end(body); } catch { res.writeHead(404).end('not found'); } }); await new Promise((r) => server.listen(0, r)); const base = `http://127.0.0.1:${server.address().port}`; await mkdir(outDir, { recursive: true }); const browser = await chromium.launch({ channel: 'msedge' }); const ctx = await browser.newContext({ ...devices['iPhone 13'], deviceScaleFactor: 2 }); const page = await ctx.newPage(); const errors = []; page.on('pageerror', (e) => errors.push(String(e))); await page.goto(base + '/'); await page.evaluate(() => { localStorage.setItem('fmode_ui_mode_override', 'mobile'); localStorage.setItem('fmode_session_token', 'demo-fmode'); localStorage.setItem('fmode_me', JSON.stringify({ id: 'me-1', name: '林一舟', mobile: '13800000000', companyName: '北美数字科技' })); }); const shots = [ ['01-messages', '/mobile/messages'], ['03-home', '/mobile/home'], ['04-contacts', '/mobile/contacts'], ['06-projects', '/mobile/projects'], ['08-tasks', '/mobile/tasks'], ['10-deliverables', '/mobile/deliverables'], ['11-events', '/mobile/events'], ['12-workbench', '/mobile/workbench'], ['13-mine', '/mobile/mine'], ]; for (const [name, path] of shots) { await page.goto(base + path, { waitUntil: 'networkidle' }); await page.waitForTimeout(900); await page.screenshot({ path: join(outDir, `${name}.png`) }); console.log('shot', name, page.url()); } // 登录页(清掉登录态) await page.goto(base + '/login'); await page.evaluate(() => { localStorage.removeItem('fmode_session_token'); localStorage.removeItem('fmode_me'); }); await page.goto(base + '/login', { waitUntil: 'networkidle' }); await page.waitForTimeout(600); await page.screenshot({ path: join(outDir, '00-login.png') }); console.log('errors:', errors.length ? errors : 'none'); await browser.close(); server.close();