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 未做部分)
241 lines
15 KiB
JavaScript
241 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
||
/* 三态主题专项验证:日间 / 夜间 / 自动 + iframe 同步。 */
|
||
import { readFileSync } from 'fs';
|
||
import { join } from 'path';
|
||
|
||
const ROOT = process.cwd();
|
||
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
|
||
let fail = 0;
|
||
const ok = (condition, message) => {
|
||
console.log((condition ? 'PASS' : 'FAIL') + ' ' + message);
|
||
if (!condition) fail++;
|
||
};
|
||
|
||
const app = readFileSync(join(ROOT, 'site/app.js'), 'utf8');
|
||
const index = readFileSync(join(ROOT, 'site/index.html'), 'utf8');
|
||
const css = readFileSync(join(ROOT, '.design_library/kole-ui/colors_and_type.css'), 'utf8');
|
||
ok(/prefers-color-scheme/.test(app), '静态:支持 prefers-color-scheme');
|
||
ok(/localStorage\.setItem\(['"]kole-mode/.test(app), '静态:持久化 kole-mode');
|
||
ok(/data-mode="auto"/.test(index), '静态:站点包含自动模式选项');
|
||
ok(/id="topbar-mode"/.test(index), '静态:顶栏含主题模式入口');
|
||
/* 顶栏菜单块内三态齐全:块内容取到悬浮菜单 id 之前(两个菜单之间只隔 fab-theme / fab-top 两个按钮),
|
||
不设长度窗口 —— 选项之间隔着内联 SVG,窗口大小会随图标改动失真 */
|
||
const topbarMenuBlock = index.slice(index.indexOf('id="topbar-mode-menu"'), index.indexOf('id="mode-menu"'));
|
||
ok(['light', 'dark', 'auto'].every((m) => topbarMenuBlock.includes(`data-mode="${m}"`)), '静态:顶栏菜单三态齐全');
|
||
ok(/html\.kole-dark/.test(css), '静态:暗色令牌入口存在');
|
||
/* 默认自动:kole-mode 缺省值必须是 auto —— 静态卡住回退成 light 的回归 */
|
||
ok(/var MODE_DEFAULT = 'auto'/.test(app), '静态:模式默认值是 auto');
|
||
ok(/'kole-mode'\) \|\| 'auto'/.test(index), '静态:解析期 bootstrap 默认 auto');
|
||
ok(index.indexOf("|| 'auto'") < index.indexOf("data.js"), '静态:bootstrap 在 data.js 之前执行(无主题闪烁)');
|
||
ok(/<meta name="color-scheme" content="light dark">/.test(index), '静态:声明 color-scheme(原生控件跟随)');
|
||
|
||
import { probeContrast } from './lib/contrast-probe.mjs';
|
||
let chromium;
|
||
try {
|
||
({ chromium } = await import('playwright'));
|
||
} catch {
|
||
console.error('[FATAL] 未安装 playwright');
|
||
process.exit(2);
|
||
}
|
||
|
||
const browser = await chromium.launch();
|
||
try {
|
||
async function siteCase(colorScheme, storedMode, expectedDark, label) {
|
||
const page = await browser.newPage({ colorScheme });
|
||
await page.addInitScript((mode) => localStorage.setItem('kole-mode', mode), storedMode);
|
||
await page.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
|
||
await page.waitForTimeout(150);
|
||
const result = await page.evaluate(() => ({
|
||
dark: document.documentElement.classList.contains('kole-dark'),
|
||
stored: localStorage.getItem('kole-mode'),
|
||
menu: document.querySelectorAll('#mode-menu [role="radio"]').length,
|
||
auto: document.querySelector('#mode-menu [data-mode="auto"]')?.getAttribute('aria-checked'),
|
||
iframe: Array.from(document.querySelectorAll('iframe')).every((f) => {
|
||
try { return f.contentDocument?.documentElement.classList.contains('kole-dark') === document.documentElement.classList.contains('kole-dark'); } catch { return true; }
|
||
})
|
||
}));
|
||
ok(result.dark === expectedDark, `${label}:kole-dark=${result.dark}`);
|
||
ok(result.stored === storedMode, `${label}:kole-mode=${result.stored}`);
|
||
ok(result.menu === 3 && result.auto === (storedMode === 'auto' ? 'true' : 'false'), `${label}:模式菜单 ARIA`);
|
||
ok(result.iframe, `${label}:iframe 主题同步`);
|
||
await page.close();
|
||
}
|
||
|
||
await siteCase('light', 'auto', false, '自动 + 系统日间');
|
||
await siteCase('dark', 'auto', true, '自动 + 系统夜间');
|
||
await siteCase('dark', 'light', false, '手动日间覆盖系统夜间');
|
||
await siteCase('light', 'dark', true, '手动夜间覆盖系统日间');
|
||
|
||
for (const mode of ['light', 'dark']) {
|
||
await chromeContrastCase(mode, '/site/guide', mode + ' · /site/guide');
|
||
await chromeContrastCase(mode, '/site/component/table', mode + ' · /site/component/table');
|
||
await chromeContrastCase(mode, '/site/component/button', mode + ' · /site/component/button');
|
||
}
|
||
|
||
/* 站点 chrome 的对比度(亮/暗双模式):徽章、警告 callout、代码块、侧栏选中态等
|
||
在两种模式下都要过 AA。判据与暗色那份共用 tools/lib/contrast-probe.mjs。 */
|
||
async function chromeContrastCase(mode, route, label) {
|
||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||
await page.addInitScript((m) => localStorage.setItem('kole-mode', m), mode);
|
||
const resp = await page.goto(BASE + route, { waitUntil: 'networkidle', timeout: 20000 }).catch(() => null);
|
||
const status = resp ? resp.status() : 0;
|
||
ok(status === 200, `${label}:可访问(HTTP ${status})`);
|
||
if (status !== 200) { await page.close(); return; }
|
||
const isDark = await page.evaluate(() => document.documentElement.classList.contains('kole-dark'));
|
||
ok(isDark === (mode === 'dark'), `${label}:kole-dark=${isDark}`);
|
||
const res = await probeContrast(page);
|
||
ok(res.bad.length === 0, `${label}:chrome 对比度抽查 ${res.checked} 节点` + (res.bad.length ? ' 不足:' + res.bad.join(';') : ''));
|
||
await page.close();
|
||
}
|
||
|
||
|
||
/* 首次访问(无 kole-mode):默认 auto,且跟随系统实时生效 */
|
||
const firstRun = await browser.newPage({ colorScheme: 'dark' });
|
||
await firstRun.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
|
||
await firstRun.waitForTimeout(150);
|
||
const first = await firstRun.evaluate(() => {
|
||
const cur = document.getElementById('mode-current');
|
||
return {
|
||
stored: localStorage.getItem('kole-mode'),
|
||
dark: document.documentElement.classList.contains('kole-dark'),
|
||
auto: document.querySelector('#mode-menu [data-mode="auto"]')?.getAttribute('aria-checked'),
|
||
current: (cur || {}).textContent || '',
|
||
currentLines: cur ? cur.children.length : 0,
|
||
currentEm: cur && cur.querySelector('em') ? cur.querySelector('em').textContent : ''
|
||
};
|
||
});
|
||
ok(first.stored === 'auto', `首访默认:kole-mode=${first.stored}(落盘默认 auto)`);
|
||
ok(first.dark === true, '首访默认:系统夜间时生效夜间');
|
||
ok(first.auto === 'true', '首访默认:菜单选中自动');
|
||
ok(first.currentLines === 2 && first.current.indexOf('自动模式') > -1, `首访默认:菜单头两行("${first.current.trim().replace(/\s+/g, ' ')}")`);
|
||
ok(first.currentEm.indexOf('夜间') > -1, `首访默认:第二行标出生效主题("${first.currentEm}")`);
|
||
await firstRun.emulateMedia({ colorScheme: 'light' });
|
||
await firstRun.waitForTimeout(120);
|
||
ok(!(await firstRun.locator('html').evaluate((el) => el.classList.contains('kole-dark'))), '首访默认:系统切日间后即时反色');
|
||
await firstRun.close();
|
||
|
||
const changing = await browser.newPage({ colorScheme: 'light' });
|
||
await changing.addInitScript(() => localStorage.setItem('kole-mode', 'auto'));
|
||
await changing.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
|
||
await changing.waitForTimeout(100);
|
||
await changing.emulateMedia({ colorScheme: 'dark' });
|
||
await changing.waitForTimeout(100);
|
||
ok(await changing.locator('html').evaluate((el) => el.classList.contains('kole-dark')), '自动模式:系统切换到夜间即时生效');
|
||
await changing.emulateMedia({ colorScheme: 'light' });
|
||
await changing.waitForTimeout(100);
|
||
ok(!(await changing.locator('html').evaluate((el) => el.classList.contains('kole-dark'))), '自动模式:系统切换到日间即时生效');
|
||
await changing.close();
|
||
|
||
const menuPage = await browser.newPage({ colorScheme: 'light' });
|
||
await menuPage.addInitScript(() => localStorage.removeItem('kole-mode'));
|
||
await menuPage.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
|
||
await menuPage.locator('#fab-mode').click();
|
||
await menuPage.locator('#mode-menu [data-mode="dark"]').click();
|
||
ok(await menuPage.evaluate(() => localStorage.getItem('kole-mode') === 'dark' && document.documentElement.classList.contains('kole-dark')), '菜单选择夜间模式');
|
||
await menuPage.close();
|
||
|
||
/* 顶栏选择器:可见、可选、与悬浮菜单互斥、窄屏收成图标 */
|
||
const topbar = await browser.newPage({ colorScheme: 'light', viewport: { width: 1280, height: 800 } });
|
||
await topbar.addInitScript(() => localStorage.removeItem('kole-mode'));
|
||
await topbar.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
|
||
await topbar.waitForTimeout(150);
|
||
ok(await topbar.locator('#topbar-mode').isVisible(), '顶栏入口:默认可⻅');
|
||
const topbarLabel = (await topbar.locator('#topbar-mode-label').textContent()).trim();
|
||
ok(topbarLabel === '自动模式', `顶栏入口:显示当前模式("${topbarLabel}")`);
|
||
await topbar.locator('#topbar-mode').click();
|
||
ok(await topbar.locator('#topbar-mode-menu').isVisible(), '顶栏入口:点开三态菜单');
|
||
ok(await topbar.locator('#topbar-mode-menu [data-mode="auto"]').getAttribute('aria-checked') === 'true', '顶栏菜单:默认选中自动');
|
||
await topbar.locator('#topbar-mode-menu [data-mode="dark"]').click();
|
||
ok(await topbar.evaluate(() => document.documentElement.classList.contains('kole-dark') && localStorage.getItem('kole-mode') === 'dark'), '顶栏菜单:选夜间即生效并落盘');
|
||
ok((await topbar.locator('#topbar-mode-label').textContent()).trim() === '夜间模式', '顶栏入口:模式名同步更新');
|
||
/* 两个入口互斥:顶栏开着时点悬浮按钮,顶栏那份必须收起 */
|
||
await topbar.locator('#topbar-mode').click();
|
||
await topbar.locator('#fab-mode').click();
|
||
ok(await topbar.evaluate(() => document.getElementById('topbar-mode-menu').classList.contains('hidden')
|
||
&& !document.getElementById('mode-menu').classList.contains('hidden')), '两个入口互斥:悬浮菜单打开时顶栏菜单收起');
|
||
await topbar.keyboard.press('Escape');
|
||
ok(await topbar.evaluate(() => document.getElementById('mode-menu').classList.contains('hidden')), 'Esc 关闭菜单');
|
||
await topbar.setViewportSize({ width: 900, height: 800 });
|
||
await topbar.waitForTimeout(120);
|
||
const narrow = await topbar.evaluate(() => {
|
||
const label = document.getElementById('topbar-mode-label');
|
||
const trigger = document.getElementById('topbar-mode');
|
||
const navToggle = document.getElementById('nav-toggle');
|
||
return {
|
||
labelHidden: getComputedStyle(label).display === 'none',
|
||
triggerVisible: trigger.getBoundingClientRect().width > 0 && trigger.getBoundingClientRect().right <= window.innerWidth,
|
||
navToggleVisible: getComputedStyle(navToggle).display !== 'none'
|
||
};
|
||
});
|
||
ok(narrow.labelHidden, '窄屏:顶栏模式名收起(仅留图标 + 箭头)');
|
||
ok(narrow.triggerVisible, '窄屏:入口仍在顶栏内可见(未被挤出屏幕)');
|
||
ok(narrow.navToggleVisible, '窄屏:汉堡按钮未被挤掉');
|
||
/* 超窄屏(320px):顶栏只留品牌标记 / 搜索 / 主题 / 汉堡,语言与框架选择器让位。
|
||
只量顶栏自身:320px 下首页内容(.stat 统计卡)另有 15px 溢出,与本改动无关,
|
||
量整页会把既有问题算到顶栏头上(既有问题已另行记录,不在此处放宽或掩盖)。 */
|
||
await topbar.setViewportSize({ width: 375, height: 700 });
|
||
await topbar.waitForTimeout(120);
|
||
const phone = await topbar.evaluate(() => {
|
||
const shown = (sel) => getComputedStyle(document.querySelector(sel)).display !== 'none';
|
||
const r = document.getElementById('topbar-mode').getBoundingClientRect();
|
||
return {
|
||
themeVisible: shown('#theme-select') && r.right <= window.innerWidth,
|
||
langVisible: shown('#lang-select')
|
||
};
|
||
});
|
||
ok(phone.themeVisible, '375px:主题入口在顶栏可见');
|
||
ok(phone.langVisible, '375px:语言选择器未被牵连隐藏(常见手机宽度仍可切语言)');
|
||
await topbar.setViewportSize({ width: 320, height: 700 });
|
||
await topbar.waitForTimeout(120);
|
||
const tiny = await topbar.evaluate(() => {
|
||
const inner = document.querySelector('.topbar-inner');
|
||
const cs = getComputedStyle(inner);
|
||
const visible = [...inner.children].filter((el) => {
|
||
const s = getComputedStyle(el);
|
||
if (s.display === 'none' || s.position === 'fixed' || s.visibility === 'hidden') return false;
|
||
return el.getBoundingClientRect().width > 0;
|
||
});
|
||
const need = visible.reduce((a, el) => a + el.getBoundingClientRect().width, 0)
|
||
+ Math.max(0, visible.length - 1) * parseFloat(cs.gap)
|
||
+ parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
|
||
const box = (sel) => document.querySelector(sel).getBoundingClientRect();
|
||
const shown = (sel) => getComputedStyle(document.querySelector(sel)).display !== 'none';
|
||
return {
|
||
topbarOverflow: Math.max(0, Math.ceil(need - document.documentElement.clientWidth)),
|
||
themeVisible: shown('#theme-select') && box('#topbar-mode').right <= window.innerWidth,
|
||
langHidden: !shown('#lang-select'),
|
||
fwHidden: !shown('.fw-select-wrap'),
|
||
navToggleVisible: shown('#nav-toggle')
|
||
};
|
||
});
|
||
ok(tiny.topbarOverflow <= 0, `超窄屏 320px:顶栏自身无横向溢出(溢出 ${tiny.topbarOverflow}px)`);
|
||
ok(tiny.themeVisible, '超窄屏 320px:主题入口仍在顶栏可见');
|
||
ok(tiny.langHidden && tiny.fwHidden, '超窄屏 320px:语言与框架选择器让位(保主题入口)');
|
||
ok(tiny.navToggleVisible, '超窄屏 320px:汉堡按钮保留');
|
||
await topbar.close();
|
||
|
||
const switcher = await browser.newPage({ colorScheme: 'light' });
|
||
await switcher.addInitScript(() => localStorage.removeItem('kole-mode'));
|
||
await switcher.goto(BASE + '/frameworks/ThemeSwitcher.html', { waitUntil: 'load', timeout: 15000 });
|
||
const modes = await switcher.locator('.kole-themesw-opt').count();
|
||
ok(modes === 3, 'ThemeSwitcher H5:三种模式');
|
||
ok(await switcher.locator('[data-mode="auto"]').getAttribute('aria-checked') === 'true', 'ThemeSwitcher H5:默认自动');
|
||
await switcher.locator('[data-mode="dark"]').click();
|
||
ok(await switcher.locator('html').evaluate((el) => el.classList.contains('kole-dark')), 'ThemeSwitcher H5:夜间模式生效');
|
||
await switcher.locator('[data-mode="auto"]').click();
|
||
ok(await switcher.locator('[data-mode="auto"]').getAttribute('aria-checked') === 'true', 'ThemeSwitcher H5:自动模式 ARIA');
|
||
await switcher.close();
|
||
|
||
const pg = await browser.newPage({ colorScheme: 'dark' });
|
||
await pg.addInitScript(() => localStorage.removeItem('kole-mode'));
|
||
await pg.goto(BASE + '/site/playground.html', { waitUntil: 'load', timeout: 15000 });
|
||
await pg.waitForTimeout(150);
|
||
ok(await pg.locator('html').evaluate((el) => el.classList.contains('kole-dark')), 'playground:默认自动 + 系统夜间生效');
|
||
await pg.close();
|
||
} finally {
|
||
await browser.close();
|
||
}
|
||
|
||
console.log(fail ? `THEME VERIFY FAIL (${fail})` : 'THEME VERIFY OK');
|
||
process.exit(fail ? 1 : 0);
|