Files
aurora-admin/tools/verify-templates.mjs
T
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

272 lines
14 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.
#!/usr/bin/env node
/**
* verify-templates.mjs — 模板页库(S4-P11)浏览器实测。
*
* 前置:仓库根目录起 dev-server(node site/dev-server.js),或设 REG_BASE 指向已跑的实例。
*
* 为什么必须真跑:模板页的价值在于「交互可用」——筛选、分页、提交反馈、抽屉、开关
* 这些都靠 JS 驱动,静态检查只能证明文件里有那几行。这里逐页走真实交互,
* 并核对「令牌生效」(组件视觉来自 colors_and_type.css / components.css,而不是自造样式)。
*/
import { join } from 'node:path';
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
const failures = [];
const checks = [];
function check(name, ok, detail = '') {
checks.push({ name, ok, detail });
if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`);
}
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
console.error('[templates] 未安装 playwright。CI 环境请先 npm i -D playwright && npx playwright install --with-deps chromium');
process.exit(2);
}
try {
const r = await fetch(`${BASE}/site/data.json`, { signal: AbortSignal.timeout(5000) });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
} catch (e) {
console.error(`[templates] 无法连接 ${BASE}/site/data.json — ${e.message}`);
console.error(' 请先在仓库根目录运行:node site/dev-server.js');
process.exit(2);
}
const PAGES = [
{ slug: 'login', title: '登录页' },
{ slug: 'dashboard', title: '数据看板' },
{ slug: 'order-list', title: '订单列表' },
{ slug: 'settings', title: '系统设置' },
];
const browser = await chromium.launch();
for (const spec of PAGES) {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const consoleErrors = [];
const badResponses = [];
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 160)); });
page.on('pageerror', (e) => consoleErrors.push('pageerror: ' + String(e.message).slice(0, 160)));
page.on('response', (res) => { if (res.status() >= 400) badResponses.push(`${res.status()} ${res.url()}`); });
await page.goto(`${BASE}/site/scenario/${spec.slug}.html`, { waitUntil: 'networkidle', timeout: 20000 });
const tag = `[${spec.slug}]`;
/* 令牌与组件样式必须真的加载并生效 */
const tokenOk = await page.evaluate(() => {
const v = getComputedStyle(document.documentElement).getPropertyValue('--kole-color-brand').trim();
const body = getComputedStyle(document.body).backgroundColor;
return { brand: v, bodyBg: body };
});
check(`${tag} 令牌加载`, tokenOk.brand !== '', `--kole-color-brand = "${tokenOk.brand}"`);
/* 页面有可见内容(不是白屏) */
const mainBox = await page.locator('main, .lg-shell, .db-shell').first().boundingBox();
check(`${tag} 非白屏`, Boolean(mainBox && mainBox.height > 200), mainBox ? `高度 ${Math.round(mainBox.height)}px` : '主体不可见');
check(`${tag} 零控制台错误`, consoleErrors.length === 0, consoleErrors.join(' | '));
check(`${tag} 零 4xx/5xx`, badResponses.length === 0, badResponses.join(' | '));
/* ── 逐页交互 ───────────────────────────────────────── */
if (spec.slug === 'login') {
/* 空提交 → 两条字段级错误提示 */
await page.locator('#lg-submit').click();
const errs = await page.locator('#lg-account-err, #lg-password-err').evaluateAll((els) => els.filter((e) => !e.hidden).map((e) => e.textContent.trim()));
check(`${tag} 空提交报错`, errs.length === 2, JSON.stringify(errs));
/* 格式错误 → 具体可操作的提示 */
await page.locator('#lg-account').fill('not-an-email');
await page.locator('#lg-password').fill('short');
await page.locator('#lg-submit').click();
const accErr = await page.locator('#lg-account-err').textContent();
const pwdErr = await page.locator('#lg-password-err').textContent();
check(`${tag} 账号格式提示`, accErr.includes('邮箱') || accErr.includes('手机号'), accErr.trim());
check(`${tag} 密码长度提示`, pwdErr.includes('8'), pwdErr.trim());
/* 密码可见性切换 */
await page.locator('#lg-eye').click();
const typeAfter = await page.locator('#lg-password').getAttribute('type');
check(`${tag} 密码显隐切换`, typeAfter === 'text', `type=${typeAfter}`);
await page.locator('#lg-eye').click();
/* 正确凭据 → 成功态 */
await page.locator('#lg-account').fill('demo@kole.ui');
await page.locator('#lg-password').fill('koleui123');
await page.locator('#lg-submit').click();
await page.waitForSelector('#lg-done:not([hidden])', { timeout: 5000 });
const doneText = await page.locator('#lg-done-sub').textContent();
check(`${tag} 提交成功反馈`, doneText.includes('demo@kole.ui'), doneText.trim());
/* 锁定账号 → 业务错误 */
await page.locator('#lg-retry').click();
await page.locator('#lg-account').fill('locked@kole.ui');
await page.locator('#lg-password').fill('koleui123');
await page.locator('#lg-submit').click();
const lockedErr = await page.locator('#lg-account-err').textContent();
check(`${tag} 账号锁定反馈`, lockedErr.includes('锁定'), lockedErr.trim());
}
if (spec.slug === 'dashboard') {
/* 指标卡有真实数字 */
const m1 = await page.locator('#db-m1').textContent();
check(`${tag} 指标卡渲染`, /¥[\d,]+/.test(m1), m1.trim());
/* 图表有数据点 */
const shapes = await page.locator('#db-svg rect, #db-svg polyline, #db-svg circle').count();
check(`${tag} 图表渲染`, shapes > 10, `${shapes} 个图元`);
/* 切换时间范围 → 数字变化 */
await page.locator('.kole-segmented-item[data-range="30"]').click();
const m2 = await page.locator('#db-m1').textContent();
check(`${tag} 范围切换生效`, m2 !== m1, `${m1.trim()} → ${m2.trim()}`);
/* 面板折叠 */
await page.locator('#db-panel-toggle').click();
const collapsed = await page.locator('#db-panel').evaluate((el) => el.classList.contains('is-collapsed'));
check(`${tag} 图表面板折叠`, collapsed === true);
/* 分页 */
const before = await page.locator('#db-tbody tr').first().textContent();
await page.locator('#db-pager button', { hasText: '下一页' }).click();
const after = await page.locator('#db-tbody tr').first().textContent();
check(`${tag} 分页切页`, before !== after, `第 1 页首行 → ${after.trim().slice(0, 20)}`);
/* 导出反馈 */
await page.locator('#db-export').click();
await page.waitForSelector('.kole-message', { timeout: 5000 });
check(`${tag} 导出反馈`, true);
}
if (spec.slug === 'order-list') {
const total = await page.locator('#ol-total').textContent();
check(`${tag} 表格渲染`, Number(total) === 37, `共 ${total} 条`);
/* 筛选:状态 = 已完成 */
await page.locator('#ol-status').selectOption('已完成');
await page.locator('#ol-filter button[type="submit"]').click();
const filtered = await page.locator('#ol-total').textContent();
const tags = await page.locator('#ol-tbody .kole-tag').allTextContents();
check(`${tag} 筛选生效`, tags.length > 0 && tags.every((t) => t.trim() === '已完成'), `命中 ${filtered} 条,状态 ${[...new Set(tags.map((t) => t.trim()))].join('/')}`);
/* 空结果 → 空状态 */
await page.locator('#ol-kw').fill('ZZZ-NOT-EXIST');
await page.locator('#ol-filter button[type="submit"]').click();
const emptyVisible = await page.locator('#ol-empty').evaluate((el) => el.classList.contains('is-open'));
check(`${tag} 空状态显示`, emptyVisible === true);
/* 清空筛选 */
await page.locator('#ol-empty-reset').click();
const restored = await page.locator('#ol-total').textContent();
check(`${tag} 清空筛选恢复`, Number(restored) === 37, `恢复 ${restored} 条`);
/* 全选 → 批量条出现 */
await page.locator('#ol-check-all').check();
const barOpen = await page.locator('#ol-batchbar').evaluate((el) => el.classList.contains('is-open'));
const selN = await page.locator('#ol-sel-count').textContent();
check(`${tag} 全选与批量条`, barOpen && Number(selN) === 8, `已选 ${selN} 项`);
/* 批量审核 → 状态流转 */
await page.locator('#ol-batch-approve').click();
await page.waitForSelector('.kole-message', { timeout: 5000 });
const msg = await page.locator('.kole-message-content').textContent();
check(`${tag} 批量审核反馈`, msg.includes('已审核') || msg.includes('没有待审核'), msg.trim());
/* 详情抽屉 */
await page.locator('#ol-tbody .ol-detail').first().click();
const drawerOpen = await page.locator('#ol-mask').evaluate((el) => el.classList.contains('is-open'));
check(`${tag} 详情抽屉打开`, drawerOpen === true);
const dlCount = await page.locator('#ol-drawer-dl dt').count();
check(`${tag} 抽屉字段渲染`, dlCount === 7, `${dlCount} 个字段`);
await page.keyboard.press('Escape');
const drawerClosed = await page.locator('#ol-mask').evaluate((el) => !el.classList.contains('is-open'));
check(`${tag} Esc 关闭抽屉`, drawerClosed === true);
/* 每页条数 */
await page.locator('#ol-size').selectOption('5');
const rows = await page.locator('#ol-tbody tr').count();
check(`${tag} 每页条数生效`, rows === 5, `${rows} 行`);
}
if (spec.slug === 'settings') {
/* 分区切换 */
await page.locator('#st-nav button[data-panel="security"]').click();
const secVisible = await page.locator('#st-panel-security').isVisible();
const basicHidden = await page.locator('#st-panel-basic').isHidden();
check(`${tag} 分区切换`, secVisible && basicHidden);
/* 开关即时生效 */
const sw = page.locator('#st-panel-security .kole-switch-switch').nth(1);
const before = await sw.getAttribute('aria-checked');
await sw.click();
const after = await sw.getAttribute('aria-checked');
check(`${tag} 开关切换`, before === 'false' && after === 'true', `${before} → ${after}`);
/* IP 白名单联动(属于保存式表单,会标脏) */
await page.locator('#st-ip-on').check();
const areaEnabled = await page.locator('#st-ip').isEnabled();
check(`${tag} IP 白名单联动`, areaEnabled === true);
/* 脏检查 → 保存。先撤销上一步的勾选,让基线回到干净态,
顺便验证「改回原值 = 自动回到干净态」这个快照式行为。 */
await page.locator('#st-ip-on').uncheck();
const backClean = await page.locator('#st-save').isDisabled();
check(`${tag} 改回原值回到干净态`, backClean === true);
await page.locator('#st-nav button[data-panel="basic"]').click();
const saveDisabledBefore = await page.locator('#st-save').isDisabled();
await page.locator('#st-name').fill('测试企业名称');
const saveEnabled = await page.locator('#st-save').isEnabled();
const dirtyVisible = await page.locator('#st-dirty').isVisible();
check(`${tag} 脏检查启用保存`, saveDisabledBefore && saveEnabled && dirtyVisible);
await page.locator('#st-save').click();
/* 保存有 700ms 的模拟延迟:等按钮文案回到「保存」再断言,
否则读到的是上一条开关提示,且后续断言会基于保存前的基线。 */
await page.waitForFunction(() => {
const b = document.getElementById('st-save');
return b && b.textContent.trim() === '保存' && b.disabled;
}, { timeout: 5000 });
/* 重开一次 toast,确保下面读到的是保存反馈而不是更早的开关提示 */
await page.waitForFunction(() => {
const m = document.querySelector('.kole-message-content');
return m && m.textContent.includes('已保存');
}, { timeout: 5000 });
const savedMsg = await page.locator('.kole-message-content').textContent();
check(`${tag} 保存反馈`, savedMsg.includes('已保存'), savedMsg.trim());
const saveDisabledAfter = await page.locator('#st-save').isDisabled();
check(`${tag} 保存后回到干净态`, saveDisabledAfter === true);
/* 放弃修改:回到「上一次保存」的值,而不是硬编码的初始值 */
await page.locator('#st-name').fill('改坏了');
await page.locator('#st-reset').click();
const restored = await page.locator('#st-name').inputValue();
check(`${tag} 放弃修改回到上次保存值`, restored === '测试企业名称', restored);
}
/* ── 响应式:无横向溢出 ─────────────────────────────── */
await page.setViewportSize({ width: 375, height: 800 });
await page.waitForTimeout(150);
const overflow375 = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
check(`${tag} 375px 无横向溢出`, overflow375 <= 1, `溢出 ${overflow375}px`);
await page.setViewportSize({ width: 1024, height: 800 });
await page.waitForTimeout(150);
const overflow1024 = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
check(`${tag} 1024px 无横向溢出`, overflow1024 <= 1, `溢出 ${overflow1024}px`);
await page.close();
}
await browser.close();
for (const c of checks) {
console.log(`[templates] ${c.ok ? 'OK' : 'FAIL'} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
}
if (failures.length) {
console.error(`\n[templates] ${failures.length} check(s) failed`);
process.exit(1);
}
console.log(`\n[templates] OK — ${checks.length} checks passed`);