65b0bb04f8
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。
167 lines
6.2 KiB
JavaScript
167 lines
6.2 KiB
JavaScript
/**
|
||
* PWA 安装性验证脚本
|
||
* 检查:
|
||
* 1. manifest.webmanifest 合法
|
||
* 2. Service Worker 注册成功
|
||
* 3. 图标全部能加载
|
||
* 4. PWA 必需字段(name, icons[192/512], start_url, display, theme_color, background_color)
|
||
* 5. 离线 fallback(/offline 或 navigateFallback 命中)
|
||
*/
|
||
import puppeteer from 'puppeteer';
|
||
import { readFile } from 'node:fs/promises';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
|
||
const URL_TO_TEST = process.env.PWA_URL || 'http://localhost:4173/login';
|
||
const CHROME_PATH =
|
||
process.env.CHROME_PATH ||
|
||
'/Users/yabozi/.cache/puppeteer/chrome/mac-148.0.7778.97/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
|
||
|
||
const checks = [];
|
||
let pass = 0;
|
||
let fail = 0;
|
||
function ok(name, detail) {
|
||
checks.push({ status: '✅', name, detail });
|
||
pass++;
|
||
}
|
||
function ko(name, detail) {
|
||
checks.push({ status: '❌', name, detail });
|
||
fail++;
|
||
}
|
||
|
||
async function fetchStatus(url) {
|
||
try {
|
||
const r = await fetch(url, { redirect: 'follow' });
|
||
return r.status;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log(`🔍 PWA check: ${URL_TO_TEST}\n`);
|
||
|
||
// 1. manifest 文件可访问 + 合法 JSON
|
||
const manifestUrl = new URL('/manifest.webmanifest', URL_TO_TEST).toString();
|
||
const manifestStatus = await fetchStatus(manifestUrl);
|
||
if (manifestStatus === 200) {
|
||
ok('manifest 200', manifestUrl);
|
||
const r = await fetch(manifestUrl);
|
||
const m = await r.json();
|
||
const required = ['name', 'short_name', 'start_url', 'display', 'icons'];
|
||
const missing = required.filter((k) => !m[k]);
|
||
if (missing.length === 0) {
|
||
ok('manifest 必备字段', Object.keys(m).join(', '));
|
||
} else {
|
||
ko('manifest 必备字段缺失', missing.join(', '));
|
||
}
|
||
// 图标尺寸
|
||
const sizes = (m.icons || []).map((i) => i.sizes).filter(Boolean);
|
||
const has192 = sizes.some((s) => s.includes('192'));
|
||
const has512 = sizes.some((s) => s.includes('512'));
|
||
const hasMaskable = (m.icons || []).some((i) => i.purpose === 'maskable');
|
||
const hasApple = (m.icons || []).some((i) =>
|
||
(i.src || '').includes('apple-touch')
|
||
);
|
||
if (has192 && has512) ok('icons 192+512', sizes.join(', '));
|
||
else ko('icons 192/512 缺失', sizes.join(', '));
|
||
if (hasMaskable) ok('maskable icon', 'present');
|
||
else ko('maskable icon', 'missing');
|
||
if (hasApple) ok('apple-touch icon', 'present');
|
||
else ko('apple-touch icon', 'missing');
|
||
} else {
|
||
ko('manifest 不可访问', `status=${manifestStatus}, url=${manifestUrl}`);
|
||
}
|
||
|
||
// 2. Service Worker 注册
|
||
const browser = await puppeteer.launch({
|
||
executablePath: CHROME_PATH,
|
||
headless: 'new',
|
||
args: [
|
||
'--no-sandbox',
|
||
'--disable-setuid-sandbox',
|
||
'--disable-dev-shm-usage',
|
||
`--user-data-dir=/tmp/lh-cache-pwa-check-${Date.now()}`,
|
||
],
|
||
});
|
||
const page = await browser.newPage();
|
||
const swLogs = [];
|
||
page.on('console', (m) => {
|
||
const t = m.text();
|
||
if (t.includes('[PWA]') || t.includes('ServiceWorker')) swLogs.push(t);
|
||
});
|
||
|
||
await page.goto(URL_TO_TEST, { waitUntil: 'networkidle0', timeout: 30000 });
|
||
// 等几秒给 SW 机会注册
|
||
await new Promise((r) => setTimeout(r, 3000));
|
||
|
||
const swInfo = await page.evaluate(async () => {
|
||
if (!('serviceWorker' in navigator)) return { supported: false };
|
||
const regs = await navigator.serviceWorker.getRegistrations();
|
||
return {
|
||
supported: true,
|
||
count: regs.length,
|
||
scopes: regs.map((r) => r.scope),
|
||
active: regs.map((r) => !!r.active),
|
||
scripts: regs.map((r) => r.active && r.active.scriptURL),
|
||
};
|
||
});
|
||
if (swInfo.supported && swInfo.count > 0 && swInfo.active.every(Boolean)) {
|
||
ok('Service Worker 已注册', `scope=${swInfo.scopes.join(',')}`);
|
||
} else if (swInfo.supported && swInfo.count > 0) {
|
||
ko('Service Worker 未激活', JSON.stringify(swInfo));
|
||
} else {
|
||
ko('Service Worker 未注册', 'getRegistrations() 为空');
|
||
}
|
||
|
||
// 3. 关键 meta 标签
|
||
const meta = await page.evaluate(() => {
|
||
const get = (sel) => document.querySelector(sel)?.getAttribute('content') || null;
|
||
return {
|
||
themeColor: get('meta[name="theme-color"]'),
|
||
appleCapable: get('meta[name="apple-mobile-web-app-capable"]'),
|
||
appleTitle: get('meta[name="apple-mobile-web-app-title"]'),
|
||
viewport: get('meta[name="viewport"]'),
|
||
manifestLink: !!document.querySelector('link[rel="manifest"]'),
|
||
appleTouchIcon: !!document.querySelector('link[rel="apple-touch-icon"]'),
|
||
};
|
||
});
|
||
if (meta.themeColor) ok('theme-color', meta.themeColor);
|
||
else ko('theme-color', 'missing');
|
||
if (meta.appleCapable === 'yes') ok('apple-mobile-web-app-capable', 'yes');
|
||
else ko('apple-mobile-web-app-capable', meta.appleCapable || 'missing');
|
||
if (meta.manifestLink) ok('manifest link', 'present');
|
||
else ko('manifest link', 'missing');
|
||
if (meta.appleTouchIcon) ok('apple-touch-icon link', 'present');
|
||
else ko('apple-touch-icon link', 'missing');
|
||
|
||
// 4. SW 日志确认 offline ready
|
||
const offlineReady = swLogs.some((l) => l.includes('离线缓存就绪') || l.includes('offline'));
|
||
if (offlineReady) ok('PWA offline log', '检测到 offline ready');
|
||
// 离线就绪不是强校验,标 warn 即可
|
||
if (!offlineReady) {
|
||
checks.push({
|
||
status: '⚠️',
|
||
name: 'PWA offline 日志',
|
||
detail: '运行后无离线 ready 日志(可能 SW 还没预缓存完,可忽略)',
|
||
});
|
||
}
|
||
|
||
await browser.close();
|
||
|
||
// 输出
|
||
for (const c of checks) {
|
||
console.log(`${c.status} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
||
}
|
||
console.log(`\n总计: ✅ ${pass} ❌ ${fail}`);
|
||
process.exit(fail > 0 ? 1 : 0);
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error('PWA check 失败:', e);
|
||
process.exit(2);
|
||
});
|