Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 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 未做部分)
272 lines
14 KiB
JavaScript
272 lines
14 KiB
JavaScript
#!/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`);
|