本提交含两条并行工作线,因互相咬合(package.json scripts、regression.yml、 npm run build 链)无法按文件干净拆分,故合并为一次自洽提交。 ## 详情页减重(本轮主任务:修复「AI 干太重」) - 导航收敛:宽屏只用右侧目录、≤1200px 只用页内 sticky 导航,纯 CSS 媒体查询 实现(不引入 JS 宽度监听)。此前两套导航同时可见,active 状态互相打架。 - 入口去重:标题区由「查看示例/在线测试/契约 JSON」三个减为「在线测试」一个; 契约 JSON 归入实现资源;删除示例底部重复的「在线测试」。 - 示例工具栏:删除与目录锚点重复的示例下拉选择器;「全部展开代码」只在 示例数 >1 时出现(103 个组件里 49 个仅 1 个示例,此前恒显示)。 - 重复文案:示例区两句同义导语合并为一句。 - 首页 CTA 由 5 个减为 2 个(浏览组件/快速开始),测试总览入口挂到已有的 通过率统计卡上,不再另占 Hero 按钮。 - 统一详情取数:抽出 fetchDetail/loadDetail 作为 details/*.json 的唯一路径, FAQ 不再自行 fetch 一遍,与组件页共用缓存与失败兜底。 ## i18n - 删除 12 组重复键(含整段 FAQ 说明),字典 604 → 585 唯一键。 - 删除本次改动产生的 6 个死键。 - verify-i18n.mjs 新增 `unique dictionary keys` 断言:重复键在对象字面量里 是静默的后值覆盖,此前无从发现;现由门禁拦住。 ## 文档事实修正(实测为准) - TESTING/CONTRIBUTING:79 → 103 组件;旧断言数改为回指 tests/report.json。 - PLATFORMS:移动端 108 文件/18 端 → 282 文件/47 端;契约 5 → 47; 令牌 17 → 15;uni-app SFC 21 → 50。 - AGENTS:断言 1405/18 页 → 1464/103 页(PC)、807/47 页(移动端)。 - package.json:YOUR-ACCOUNT 占位 → gitea 实址与 kole-ui.mymoyu.top。 ## 品牌标识(并行会话成果,一并入库) - brand-mark.json 收归真源,build:brand 生成 favicon 与单色 SVG; PC 与移动端共用资产,verify:brand 24 条断言。 - regression.yml 增加 verify:brand 步骤。 ## 门禁与验收 新增工具:verify-component-page.mjs(86 条真实浏览器断言,随详情页改造同步 更新为「只允许一套导航可见」)、verify-brand-mark.mjs、lib/i18n-dead-keys.mjs (只读诊断)。 回归:PC 100%(1464/1464,103 页,N/A 50)· 移动端 100%(807/807,47 页)。 门禁:component-page 86 · i18n 17 · brand 24 · routes all · smoke all · theme OK · isolation 31 · nav all · examples 9 · api-docs 10 · mobile-docs 12。 已知未做:i18n 另有约 44 条历史死键(非本次产生),已记为 ROADMAP S8-P6; 顶栏与悬浮区的两个主题入口为刻意设计(verify-theme 断言其互斥),未删。
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
import { chromium } from 'playwright';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
|
||||
const failures = [];
|
||||
let checks = 0;
|
||||
function check(ok, message) {
|
||||
checks++;
|
||||
if (!ok) failures.push(message);
|
||||
console.log(`[component-page] ${ok ? 'OK' : 'FAIL'} ${message}`);
|
||||
}
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('kole-lang', 'zh');
|
||||
localStorage.setItem('kole-mode', 'light');
|
||||
});
|
||||
async function ready(slug, suffix = 'h5') {
|
||||
await page.goto(`${BASE}/site/component/${slug}/${suffix}`, { waitUntil: 'networkidle' });
|
||||
await page.waitForFunction(() => {
|
||||
const root = document.querySelector('.kole-component-page');
|
||||
return root && root.dataset.examplesReady !== 'pending' && root.dataset.detailReady !== 'pending';
|
||||
});
|
||||
}
|
||||
/* 宽屏(>1200px)右侧目录是唯一导航,窄屏它被隐藏、页内 sticky 导航接管。
|
||||
active 状态与可达性都要按当前生效的那套来判,不能要求两套同时可见。 */
|
||||
const visibleNav = () => page.evaluate(() => {
|
||||
const toc = document.querySelector('#toc');
|
||||
const localNav = document.querySelector('.kole-component-nav');
|
||||
const tocVisible = !!toc && getComputedStyle(toc).display !== 'none' && toc.getBoundingClientRect().width > 0;
|
||||
const navVisible = !!localNav && !localNav.hidden && localNav.getBoundingClientRect().width > 0;
|
||||
return { tocVisible, navVisible };
|
||||
});
|
||||
try {
|
||||
const served = await (await page.request.get(`${BASE}/site/app.js`)).text();
|
||||
check(served === readFileSync(new URL('../site/app.js', import.meta.url), 'utf8'), 'preview serves the current workspace app.js');
|
||||
for (const slug of ['button', 'input', 'table', 'modal', 'dragupload', 'icon']) {
|
||||
await ready(slug);
|
||||
const structure = await page.evaluate(() => {
|
||||
const root = document.querySelector('.kole-component-page');
|
||||
const sections = [...root.querySelectorAll(':scope > section')].map(x => x.id);
|
||||
const ids = [...root.querySelectorAll('[id]')].map(x => x.id);
|
||||
const anchors = [...document.querySelectorAll('#toc [data-anchor], .kole-component-nav [data-anchor]')];
|
||||
const toc = [...document.querySelectorAll('#toc [data-anchor]')].map(x => document.getElementById(x.dataset.anchor));
|
||||
return {
|
||||
sections,
|
||||
unique: new Set(ids).size === ids.length,
|
||||
validAnchors: anchors.every(x => document.getElementById(x.dataset.anchor)),
|
||||
tocOrdered: toc.every((x, i) => !i || !!(toc[i-1].compareDocumentPosition(x) & Node.DOCUMENT_POSITION_FOLLOWING)),
|
||||
exampleCount: root.querySelectorAll('.ex-card').length,
|
||||
openCode: root.querySelectorAll('.ex-code:not([hidden])').length,
|
||||
hasApi: !!root.querySelector('#api'),
|
||||
apiLink: !!root.querySelector('.kole-component-nav [data-anchor="api"]'),
|
||||
/* #toc 在 .kole-component-page 之外(右侧栏),必须用文档级查询 ——
|
||||
用 root.querySelector('#toc …') 会恒为 null,让这条断言变成假通过。 */
|
||||
tocApiLink: !!document.querySelector('#toc [data-anchor="api"]'),
|
||||
reference: !!root.querySelector('#design #spec .kole-spec-disclosure'),
|
||||
wrappedTables: [...root.querySelectorAll('table')].every(x => x.parentElement.classList.contains('kole-doc-table')),
|
||||
pickers: root.querySelectorAll('.kole-example-picker').length,
|
||||
expandAll: root.querySelectorAll('.kole-expand-code').length
|
||||
};
|
||||
});
|
||||
check(structure.sections[0] === 'demo' && structure.sections.includes('design'), `${slug}: examples precede reference material`);
|
||||
check(structure.unique && structure.validAnchors && structure.tocOrdered, `${slug}: unique anchors and TOC match document order`);
|
||||
check(structure.exampleCount === 0 || structure.openCode === 1, `${slug}: first example code is open, remaining code is folded`);
|
||||
check(structure.hasApi === structure.tocApiLink, `${slug}: API navigation matches available data`);
|
||||
check(structure.reference && structure.wrappedTables, `${slug}: specification and tables remain accessible`);
|
||||
/* 入口减重:示例选择器已删(跳转由目录承担);
|
||||
「全部展开代码」只在多示例时出现 —— 单示例时它没有可批量操作的对象。 */
|
||||
check(structure.pickers === 0, `${slug}: the duplicate example picker is gone`);
|
||||
check(structure.expandAll === (structure.exampleCount > 1 ? 1 : 0), `${slug}: expand-all only exists for multi-example pages (examples=${structure.exampleCount}, buttons=${structure.expandAll})`);
|
||||
const nav = await visibleNav();
|
||||
check(nav.tocVisible !== nav.navVisible, `${slug}: exactly one navigation is visible at 1440px (toc=${nav.tocVisible}, local=${nav.navVisible})`);
|
||||
}
|
||||
|
||||
await ready('button');
|
||||
const payload = JSON.parse(readFileSync(new URL('../site/examples/button.json', import.meta.url), 'utf8'));
|
||||
const first = page.locator('.ex-card').first();
|
||||
const firstId = await first.getAttribute('id');
|
||||
await page.evaluate(() => document.querySelector('.ex-card').dataset.identity = 'retained');
|
||||
await first.locator('.kole-example-toggle').focus();
|
||||
await page.keyboard.press('Enter');
|
||||
check(await first.locator('.ex-code').isHidden() && await first.locator('.kole-example-toggle').getAttribute('aria-expanded') === 'false', 'keyboard folds the code with accurate aria-expanded');
|
||||
await page.locator('.kole-expand-code').click();
|
||||
check(await page.locator('.ex-code[hidden]').count() === 0, 'expand all opens every usage snippet');
|
||||
await first.locator('.ex-tabs [data-kind="jsx"]').click();
|
||||
check((await first.locator('code').textContent()) === payload.examples[0].code.jsx, 'React tab shows the exact generated usage snippet');
|
||||
check(await first.locator('.ex-tabs [aria-pressed="true"]').count() === 1, 'exactly one example stack is selected');
|
||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
await first.locator('.code-copy').click();
|
||||
await page.waitForFunction(() => document.querySelector('.ex-card .code-copy').textContent.includes('已复制'));
|
||||
const copied = await page.evaluate(() => navigator.clipboard.readText());
|
||||
// Windows clipboard normalizes LF to CRLF; only normalize line endings, not text or whitespace.
|
||||
// A missing prop, different stack or altered indentation must still fail this exact comparison.
|
||||
check(copied.replace(/\r\n/g, '\n') === payload.examples[0].code.jsx.replace(/\r\n/g, '\n'), 'copy uses the active stack snippet');
|
||||
await page.locator('.kole-expand-code').click();
|
||||
check(await page.locator('.ex-code:not([hidden])').count() === 0, 'collapse all folds every snippet');
|
||||
|
||||
/* 宽屏用右侧目录做节间跳转(页内 sticky 导航此时隐藏) */
|
||||
await page.locator('#toc [data-anchor="api"]').click();
|
||||
await page.waitForFunction(() => document.querySelector('#toc [data-anchor="api"]').getAttribute('aria-current') === 'location');
|
||||
check(await page.locator('.ex-card').first().getAttribute('data-identity') === 'retained', 'section navigation does not recreate previews');
|
||||
const apiTop = await page.locator('#api').evaluate(x => x.getBoundingClientRect().top);
|
||||
check(apiTop >= 0 && apiTop < 180, `API heading lands below the top bar after anchor navigation (top=${Math.round(apiTop)})`);
|
||||
|
||||
const lastId = payload.examples[payload.examples.length - 1].id;
|
||||
await page.locator(`#toc [data-anchor="${lastId}"]`).click();
|
||||
await page.waitForFunction(id => {
|
||||
const node = document.getElementById(id);
|
||||
return node && node.getBoundingClientRect().top < 260;
|
||||
}, lastId);
|
||||
check(new URL(page.url()).hash === '#' + lastId, 'example TOC link creates a shareable section anchor');
|
||||
await page.reload({ waitUntil: 'networkidle' });
|
||||
await page.waitForFunction(id => {
|
||||
const node = document.getElementById(id);
|
||||
return node && node.getBoundingClientRect().top >= 0 && node.getBoundingClientRect().top < 260;
|
||||
}, lastId);
|
||||
check(true, 'example deep link survives reload and async content');
|
||||
await page.locator('#toc [data-anchor="spec"]').click();
|
||||
check(await page.locator('.kole-spec-disclosure').getAttribute('open') !== null, 'specification TOC link opens the disclosure');
|
||||
|
||||
await ready('button', 'react');
|
||||
check(await page.locator('.ex-card').first().locator('.ex-tabs [aria-pressed="true"]').getAttribute('data-kind') === 'jsx', 'framework deep link selects matching usage code');
|
||||
await page.locator('#lang-trigger').click();
|
||||
await page.locator('#lang-menu [data-lang="en"]').click();
|
||||
check((await page.locator('#toc').textContent()).includes('Design & resources'), 'English section labels are translated in the on-page TOC');
|
||||
check(await page.locator('.ex-card').first().locator('.ex-tabs [aria-pressed="true"]').getAttribute('data-kind') === 'jsx', 'language switch preserves framework');
|
||||
/* 窄屏:右侧目录隐藏,页内 sticky 导航接管 —— 它必须仍然完整可达 */
|
||||
for (const width of [320, 375, 768, 1024, 1280, 1440]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const layout = await page.evaluate(() => ({
|
||||
overflow: document.documentElement.scrollWidth - innerWidth,
|
||||
nav: [...document.querySelectorAll('.kole-component-nav a')].every(x => {
|
||||
const r = x.getBoundingClientRect();
|
||||
return r.width > 0 && r.left >= 0 && r.right <= innerWidth;
|
||||
}),
|
||||
tables: [...document.querySelectorAll('.kole-doc-table')].every(x => x.getBoundingClientRect().width <= innerWidth)
|
||||
}));
|
||||
check(layout.overflow <= 1, `English ${width}px: no page overflow (overflow=${layout.overflow})`);
|
||||
check(layout.tables, `English ${width}px: data tables stay inside the viewport`);
|
||||
const nav = await visibleNav();
|
||||
check(nav.tocVisible !== nav.navVisible, `English ${width}px: exactly one navigation is visible (toc=${nav.tocVisible}, local=${nav.navVisible})`);
|
||||
if (nav.navVisible) check(layout.nav, `English ${width}px: every local-navigation control is reachable`);
|
||||
}
|
||||
check(errors.length === 0, `no uncaught page errors${errors.length ? ': ' + errors.join('; ') : ''}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
if (failures.length) {
|
||||
console.error(`[component-page] FAIL — ${failures.length}/${checks} checks failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[component-page] OK — ${checks} checks passed`);
|
||||
Reference in New Issue
Block a user