Files
CarLog/client/scripts/check-pwa.mjs
T
wsh5485 fe17886ac4 feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删
- AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3
- 化学品 / Grocy 实例对接 + 库存镜像同步
- 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护
- 月度报表:Excel 6 sheet + PDF
- PWA:manifest / SW / 离线缓存 / iOS 引导
- 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿
- 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就
- 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等
- 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications)
- 部署:宝塔面板文档 + PM2 + Nginx
2026-06-20 21:11:54 +08:00

167 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});