Files
aurora-admin/tools/verify-mobile-site.mjs
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

211 lines
9.2 KiB
JavaScript
Raw Permalink 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.
#!/usr/bin/env node
/**
* verify-mobile-site.mjs — 移动端文档站 / 测试站的浏览器实测门禁
*
* 为什么需要它(本次实测踩到):`site/m/component/<slug>.html` 里的资源路径写成了两级
* `../../.design_library/...`,从 `/site/m/component/` 解析成 `/site/.design_library/...` → **404**。
* 页面照样能打开、文字照样在、结构断言全过——**只有真浏览器会告诉你样式与演示帧一起丢了**
* (实测控制台各 2 条错误)。这正是本仓库 S5-P22/S5-P23 反复证明的那条:静态检查只能证明
* 文件里有那几行,看不到"到底加载没加载、落在哪个 URL"。
*
* 检查项(每条都必须在真浏览器里取值):
* 1. 每页 HTTP 200
* 2. 控制台 0 错误、0 未捕获异常
* 3. 总览页与组件页的演示帧全部真实渲染(contentDocument 有内容且不是 404 页)
* 4. 移动端令牌层已生效(--kole-m-touch-target 解析为 44px)
* 5. 移动端站不引用 PC 侧样式(site/style.css 不得出现)
*
* 前置:node site/dev-server.js(或 KOLE_PORT=<端口> node site/dev-server.js)
* 依赖:playwright(devDependency)
* 退出码:0 全通过 / 1 有失败 / 2 环境问题
*/
import { readFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
let pass = 0;
const failures = [];
function check(ok, id, detail) {
if (ok) {
pass++;
console.log(` PASS ${id}${detail ? ' — ' + detail : ''}`);
} else {
failures.push(id + (detail ? ' — ' + detail : ''));
console.log(` FAIL ${id}${detail ? ' — ' + detail : ''}`);
}
}
/* ---------- 待测页面清单(从索引生成,不写死) ---------- */
const indexPath = join(ROOT, '.design_library', 'kole-ui-mobile', 'components', 'index.json');
if (!existsSync(indexPath)) {
console.error('[FATAL] 缺少移动端索引:' + indexPath);
process.exit(2);
}
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const pages = [
{ url: '/site/m/index.html', kind: 'overview' },
...index.components.map((c) => ({ url: `/site/m/component/${c.slug}.html`, kind: 'component' })),
{ url: '/tests/mobile/index.html', kind: 'plain' },
{ url: '/tests/mobile/_collect.html', kind: 'collector' },
];
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
console.error('[FATAL] 未安装 playwright。CI 环境请先运行:npm i -D playwright && npx playwright install --with-deps chromium');
process.exit(2);
}
/* 前置自检:服务是否在跑 */
try {
const r = await fetch(`${BASE}/site/m/data.mobile.json`, { signal: AbortSignal.timeout(5000) });
if (!r.ok) throw new Error('HTTP ' + r.status);
} catch (e) {
console.error(`[FATAL] 无法连接 ${BASE}/site/m/data.mobile.json — ${e.message}`);
console.error(' 请先运行:node site/dev-server.js(若 3311 被占用可 KOLE_PORT=13511 node site/dev-server.js)');
process.exit(2);
}
console.log('移动端站点浏览器实测门禁');
console.log(`目标 ${BASE} · 页面 ${pages.length} 个`);
const browser = await chromium.launch();
let consoleErrorsTotal = 0;
let framesOkTotal = 0;
let framesTotal = 0;
for (const page of pages) {
const p = await browser.newPage({ viewport: { width: 1280, height: 900 } });
const errors = [];
p.on('console', (m) => {
if (m.type() === 'error') errors.push(m.text().slice(0, 160));
});
p.on('pageerror', (e) => errors.push('pageerror: ' + String(e.message).slice(0, 160)));
p.on('requestfailed', (r) => {
/* ERR_ABORTED 不算错误:收集器(_collect.html)在帧装载完成后会**主动**
frame.removeAttribute('src') 释放帧,这会中断在途请求 —— 是它的设计行为,
不是资源缺失。真正的资源问题表现为 404/ERR_INVALID_URL 等。 */
const kind = r.failure()?.errorText || '?';
if (kind === 'net::ERR_ABORTED') return;
errors.push('requestfailed: ' + r.url().slice(0, 120) + ' (' + kind + ')');
});
let status = 0;
try {
const resp = await p.goto(BASE + page.url, { waitUntil: 'load', timeout: 20000 });
status = resp ? resp.status() : 0;
await p.waitForTimeout(1200);
/* 总览页的预览帧是 loading="lazy":视口外的帧**不会**加载(浏览器行为,不是缺陷)。
实测:26 个卡片只加载了前 20 个,滚到底后 26/26。卡片多了以后(47 个)
一次 scrollTo 到底还会漏 —— 因为滚动过程中被跳过的帧不保证触发加载。
故:逐段滚动(每次一屏),再回到顶部,等全部帧有内容或超时。 */
if (page.kind === 'overview') {
const height = await p.evaluate(() => document.body.scrollHeight);
for (let y = 0; y < height; y += 800) {
await p.evaluate((top) => window.scrollTo(0, top), y);
await p.waitForTimeout(250);
}
await p.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await p.waitForTimeout(1200);
await p.evaluate(() => window.scrollTo(0, 0));
await p.waitForTimeout(600);
}
} catch (e) {
errors.push('goto: ' + String(e.message).slice(0, 120));
}
const probe = await p.evaluate(() => {
const frames = [...document.querySelectorAll('iframe')];
const okFrames = frames.filter((f) => {
try {
const d = f.contentDocument;
return !!d && !!d.body && d.body.children.length > 0 && !/404 Not Found|Cannot GET/i.test(d.body.innerText || '');
} catch (e) {
return false;
}
});
const styles = [...document.querySelectorAll('link[rel="stylesheet"]')].map((l) => l.getAttribute('href') || '');
return {
frames: frames.length,
okFrames: okFrames.length,
touchToken: getComputedStyle(document.documentElement).getPropertyValue('--kole-m-touch-target').trim(),
usesPcStyles: styles.some((h) => /(^|\/)site\/style\.css$|\/site\/style\.css/.test(h)),
};
});
await p.close();
consoleErrorsTotal += errors.length;
/* 汇总只统计被断言过的演示帧(总览页 / 组件页);收集器的 4 个帧是并发加载器,不在此列 */
if (page.kind === 'overview' || page.kind === 'component') {
framesTotal += probe.frames;
framesOkTotal += probe.okFrames;
}
const label = page.url;
check(status === 200, `${label} HTTP 200`, status === 200 ? null : '实际 ' + status);
check(errors.length === 0, `${label} 控制台 0 错误`, errors.length ? errors.slice(0, 2).join(' || ') : null);
check(probe.touchToken === '44px', `${label} 移动端令牌已生效`, probe.touchToken === '44px' ? null : `--kole-m-touch-target="${probe.touchToken}"`);
check(!probe.usesPcStyles, `${label} 未引用 PC 站样式`, probe.usesPcStyles ? '命中 site/style.css' : null);
if (page.kind === 'overview' || page.kind === 'component') {
check(
probe.frames > 0 && probe.okFrames === probe.frames,
`${label} 演示帧 ${probe.okFrames}/${probe.frames} 正常渲染`,
probe.frames === 0 ? '未找到 iframe' : null
);
}
}
await browser.close();
/* ---------- 窄屏无横向溢出 ----------
移动端文档站必须自己在手机上能用。实测(2026-09-20,375px)本站在小屏上横向溢出 128px:
元凶是表格里不可断行的长路径(`.design_library/kole-ui-mobile/components/` 把表格最小宽度顶到 462px)
与组件页固定 375px 的设备帧。修完加这道门禁 —— 只靠"在桌面宽度看一眼"是发现不了的。 */
const NARROW_WIDTHS = [320, 375, 414, 768];
const NARROW_PAGES = [
'/site/m/index.html',
`/site/m/component/${index.components[0].slug}.html`,
'/tests/mobile/index.html',
];
const narrowBrowser = await chromium.launch();
for (const url of NARROW_PAGES) {
const p = await narrowBrowser.newPage({ viewport: { width: 1280, height: 820 } });
const errors = [];
p.on('console', (m) => {
if (m.type() === 'error') errors.push(m.text().slice(0, 120));
});
for (const w of NARROW_WIDTHS) {
await p.setViewportSize({ width: w, height: 820 });
try {
await p.goto(BASE + url, { waitUntil: 'load', timeout: 20000 });
await p.waitForTimeout(500);
} catch (e) {
errors.push('goto@' + w + ': ' + String(e.message).slice(0, 80));
}
const over = await p.evaluate((width) => document.documentElement.scrollWidth - width, w);
check(over <= 1, `${url} @${w}px 无横向溢出`, over > 1 ? '溢出 ' + over + 'px' : null);
}
await p.close();
check(errors.length === 0, `${url} 窄屏全程 0 控制台错误`, errors.length ? errors.slice(0, 2).join(' || ') : null);
}
await narrowBrowser.close();
console.log('');
console.log(`汇总:控制台错误 ${consoleErrorsTotal} 条 · 演示帧 ${framesOkTotal}/${framesTotal} 正常`);
console.log('\n────────────────────────────');
if (failures.length) {
console.error(`[FAIL] ${failures.length} 条移动端站点断言失败(通过 ${pass} 条):`);
failures.forEach((f) => console.error(' - ' + f));
process.exit(1);
}
console.log(`[OK] 移动端站点全部通过(${pass} 条断言 · ${pages.length} 页)`);