fix(ui,a11y): 修 logo 深色模式不可见 + 补 tailwind font-calligraphy 工具类
修复 A:header logo 深色模式字被吞 - logo.svg 右侧公司名(睿新致遠 + NOVALON)硬编码 fill='#0A0E14', 深色背景下 background 同色不可见,红色印章下书法字与英文全部被吞。 - 复用既有 logo-white.svg(白字 + 红印章)作为深色版, header 渲染双 <Image>,按 html[data-theme] CSS 显隐。 - 双 Image 不占位(display:none),零 JS 闪烁,未来主题切换器自动生效。 修复 B:font-calligraphy 工具类不生成 - tailwind.config.js 的 fontFamily 此前未声明 calligraphy key, 导致 font-calligraphy utility 永不生成,4 处装饰书法字 (BrandStamp「值得信赖」/ ChallengeSection「使命」/ WhyUsSection「睿」「我们」) 静默回退 sans。 - fontFamily 加 calligraphy: ['var(--font-calligraphy-system)']。 审计脚本: - logo-compare*.mjs:生产 vs 本地 logo 对比定位 - logo-dark-verify.mjs:明暗双版渲染验证 - font-calligraphy-verify.mjs:合成探针验证 utility 生效 验证: - type-check 0 error · lint 0 error(127 warnings 既有项与本次无关) - 深浅色 header 截图:明暗版正确切换,书法字 + NOVALON 清晰可见 - font-calligraphy 合成探针 computed fontFamily = STKaiti, KaiTi, 楷体, SimKai, serif ✓
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// 终极:直接构造一个含 font-calligraphy className 的测试元素,
|
||||
// 把它注入到任意真实页面 DOM,读取 computed fontFamily —— 验证 CSS 规则真生效
|
||||
import { chromium } from 'playwright';
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||||
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
// 找一个已渲染的根节点挂测试元素
|
||||
const mount = document.body;
|
||||
const probe = document.createElement('span');
|
||||
probe.id = '__probe_font_calligraphy__';
|
||||
probe.className = 'font-calligraphy';
|
||||
probe.textContent = '使命';
|
||||
probe.style.position = 'fixed';
|
||||
probe.style.top = '-9999px';
|
||||
mount.appendChild(probe);
|
||||
const cs = getComputedStyle(probe);
|
||||
const out = {
|
||||
probeFontFamily: cs.fontFamily,
|
||||
probeClassList: Array.from(probe.classList),
|
||||
};
|
||||
probe.remove();
|
||||
return out;
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
const expectedPrefix = 'STKaiti';
|
||||
const pass = result.probeFontFamily.startsWith(expectedPrefix);
|
||||
console.log(`\n[assert] probe computed fontFamily starts with ${expectedPrefix}: ${pass}`);
|
||||
console.log(`[result] ${pass ? 'PASS' : 'FAIL'}`);
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 深色模式 logo 对比:生产 vs 本地
|
||||
* 强制 prefers-color-scheme: dark,截 header logo
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const TARGETS = [
|
||||
{ tag: 'prod-dark', url: 'https://www.novalon.cn/' },
|
||||
{ tag: 'local-dark', url: 'http://127.0.0.1:3000/' },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
for (const t of TARGETS) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
colorScheme: 'dark', // 强制深色模式
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// 1) 截 header
|
||||
const header = page.locator('header').first();
|
||||
await header.screenshot({ path: path.join(OUT, `${t.tag}-header.png`) }).catch(async () => {
|
||||
await page.screenshot({ path: path.join(OUT, `${t.tag}-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||
});
|
||||
|
||||
// 2) 探测 logo 元素的可视状态(在深色背景下)
|
||||
const info = await page.evaluate(() => {
|
||||
const imgs = [...document.querySelectorAll('img')].filter(i => /logo\.svg$/.test(i.getAttribute('src') || ''));
|
||||
return imgs.map(i => {
|
||||
const r = i.getBoundingClientRect();
|
||||
const cs = getComputedStyle(i);
|
||||
const parentCs = getComputedStyle(i.closest('header') || document.body);
|
||||
return {
|
||||
src: i.getAttribute('src'),
|
||||
renderedW: Math.round(r.width),
|
||||
renderedH: Math.round(r.height),
|
||||
filter: cs.filter,
|
||||
opacity: cs.opacity,
|
||||
visibility: cs.visibility,
|
||||
parentBg: parentCs.backgroundColor,
|
||||
htmlBg: getComputedStyle(document.documentElement).backgroundColor,
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log(`[${t.tag}]`);
|
||||
for (const l of info) console.log(' ' + JSON.stringify(l));
|
||||
} catch (e) {
|
||||
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 120)}`);
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log('\n✓ 输出目录:' + OUT);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 综合视觉对比:生产 vs 本地,多个关键位置
|
||||
* 收集决定性证据,让用户指出"logo 原本是书法体但生产不是"指的具体位置
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const PAGES = [
|
||||
{ name: 'home', url: '/' },
|
||||
{ name: 'about-brand', url: '/about/brand' },
|
||||
{ name: 'about', url: '/about' },
|
||||
];
|
||||
|
||||
const TARGETS = [
|
||||
{ tag: 'prod', base: 'https://www.novalon.cn' },
|
||||
{ tag: 'local', base: 'http://127.0.0.1:3000' },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
|
||||
for (const t of TARGETS) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
for (const p of PAGES) {
|
||||
const url = t.base + p.url;
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
// 截首屏(Hero + header 区域)
|
||||
await page.screenshot({
|
||||
path: path.join(OUT, `${t.tag}-${p.name}-fold.png`),
|
||||
clip: { x: 0, y: 0, width: 1440, height: 900 },
|
||||
});
|
||||
console.log(`[${t.tag}] ${p.name} OK`);
|
||||
} catch (e) {
|
||||
console.log(`[${t.tag}] ${p.name} FAILED ${e.message.slice(0, 100)}`);
|
||||
}
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log('\n✓ 输出:' + OUT);
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 模拟点深色模式按钮后的 header 截图
|
||||
* 找切换按钮:theme toggle / data-theme / aria-label 含亮暗
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const TARGETS = [
|
||||
{ tag: 'prod', url: 'https://www.novalon.cn/' },
|
||||
{ tag: 'local', url: 'http://127.0.0.1:3000/' },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
for (const t of TARGETS) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// 1) 找主题切换按钮
|
||||
const btnInfo = await page.evaluate(() => {
|
||||
const candidates = [
|
||||
...document.querySelectorAll('header button, header [role="button"], [data-theme-toggle], [aria-label*="深" i], [aria-label*="暗" i], [aria-label*="dark" i], [aria-label*="theme" i], [aria-label*="切换" i]'),
|
||||
];
|
||||
return candidates.map(b => ({
|
||||
tag: b.tagName,
|
||||
label: b.getAttribute('aria-label'),
|
||||
text: (b.textContent || '').slice(0, 30),
|
||||
className: (b.className || '').toString().slice(0, 60),
|
||||
data: JSON.stringify(b.dataset).slice(0, 80),
|
||||
})).slice(0, 6);
|
||||
});
|
||||
console.log(`[${t.tag}] 候选切换按钮:`, JSON.stringify(btnInfo, null, 2));
|
||||
|
||||
// 2) 直接通过 localStorage 强制 dark(更稳)
|
||||
await page.evaluate(() => {
|
||||
try { localStorage.setItem('novalon-theme', 'dark'); } catch {}
|
||||
});
|
||||
await page.reload({ waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// 3) 截深色模式下的 header
|
||||
const header = page.locator('header').first();
|
||||
await header.screenshot({ path: path.join(OUT, `${t.tag}-dark-header.png`) }).catch(async () => {
|
||||
await page.screenshot({ path: path.join(OUT, `${t.tag}-dark-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||
});
|
||||
|
||||
// 4) 探测 logo 在深色模式下的实际颜色
|
||||
const logoState = await page.evaluate(() => {
|
||||
const img = [...document.querySelectorAll('img')].find(i => /logo\.svg$/.test(i.getAttribute('src') || ''));
|
||||
if (!img) return { found: false };
|
||||
const r = img.getBoundingClientRect();
|
||||
const parent = img.closest('header') || document.body;
|
||||
return {
|
||||
found: true,
|
||||
src: img.getAttribute('src'),
|
||||
renderedW: Math.round(r.width),
|
||||
renderedH: Math.round(r.height),
|
||||
parentBg: getComputedStyle(parent).backgroundColor,
|
||||
htmlClass: document.documentElement.className,
|
||||
htmlDataTheme: document.documentElement.getAttribute('data-theme'),
|
||||
};
|
||||
});
|
||||
console.log(`[${t.tag}] dark-mode logo state:`, JSON.stringify(logoState, null, 2));
|
||||
} catch (e) {
|
||||
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 200)}`);
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log('\n✓ 输出:' + OUT);
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Logo 对比诊断:生产环境 vs 本地 dev
|
||||
*
|
||||
* 用法:node scripts/audit/logo-compare.mjs
|
||||
* 输出:dogfood-ui-audit/logo-compare/{prod,local}-header.png + logo-meta.json
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const TARGETS = [
|
||||
{ tag: 'prod', url: 'https://www.novalon.cn/' },
|
||||
{ tag: 'local', url: 'http://127.0.0.1:3000/' },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const meta = {};
|
||||
|
||||
for (const t of TARGETS) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
const errors = [];
|
||||
page.on('requestfailed', r => errors.push('FAILED ' + r.url()));
|
||||
page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE ' + m.text()); });
|
||||
|
||||
try {
|
||||
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// header logo 元素探测
|
||||
const info = await page.evaluate(() => {
|
||||
const imgs = [...document.querySelectorAll('img')]
|
||||
.filter(i => /logo/i.test(i.getAttribute('src') || ''));
|
||||
return imgs.map(i => {
|
||||
const r = i.getBoundingClientRect();
|
||||
const cs = getComputedStyle(i);
|
||||
return {
|
||||
src: i.getAttribute('src'),
|
||||
currentSrc: i.currentSrc,
|
||||
renderedW: Math.round(r.width),
|
||||
renderedH: Math.round(r.height),
|
||||
naturalW: i.naturalWidth,
|
||||
naturalH: i.naturalHeight,
|
||||
complete: i.complete,
|
||||
fontFamily: cs.fontFamily,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 截 header 区域(放大到 logo 可见)
|
||||
const header = page.locator('header').first();
|
||||
await header.screenshot({ path: path.join(OUT, `${t.tag}-header.png`) }).catch(async () => {
|
||||
await page.screenshot({ path: path.join(OUT, `${t.tag}-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||
});
|
||||
|
||||
meta[t.tag] = { url: t.url, logos: info, errors };
|
||||
console.log(`[${t.tag}] OK logos=${info.length} errors=${errors.length}`);
|
||||
for (const l of info) {
|
||||
console.log(` src=${l.src} currentSrc=${l.currentSrc}`);
|
||||
console.log(` rendered=${l.renderedW}x${l.renderedH} natural=${l.naturalW}x${l.naturalH} complete=${l.complete}`);
|
||||
}
|
||||
} catch (e) {
|
||||
meta[t.tag] = { url: t.url, error: e.message.slice(0, 200), errors };
|
||||
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 120)}`);
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'logo-meta.json'), JSON.stringify(meta, null, 2));
|
||||
await browser.close();
|
||||
console.log('\n✓ 输出目录:' + OUT);
|
||||
@@ -0,0 +1,55 @@
|
||||
// 验证 header logo 明暗双版:浅色应显示 logo.svg(黑字),深色应显示 logo-white.svg(白字)
|
||||
// 用法:node scripts/audit/logo-dark-verify.mjs
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = 'http://127.0.0.1:3000';
|
||||
const OUT = 'dogfood-output/logo-dark-verify';
|
||||
|
||||
const shots = [];
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// 浅色(默认)
|
||||
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(800);
|
||||
const lightSrc = await page.$eval('header img', (el) => el.getAttribute('src'));
|
||||
const visibleLight = await getVisible(page);
|
||||
await page.screenshot({ path: `${OUT}/header-light.png` });
|
||||
shots.push({ theme: 'light', htmlDataTheme: await page.getAttribute('html', 'data-theme'), firstImgSrc: lightSrc, visible: visibleLight });
|
||||
|
||||
// 深色:设 localStorage 后 reload(与站点内联脚本一致)
|
||||
await page.evaluate(() => localStorage.setItem('novalon-theme', 'dark'));
|
||||
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(800);
|
||||
const visibleDark = await getVisible(page);
|
||||
const darkSrc = await page.$eval('header img', (el) => el.getAttribute('src')).catch(() => 'N/A');
|
||||
await page.screenshot({ path: `${OUT}/header-dark.png` });
|
||||
shots.push({ theme: 'dark', htmlDataTheme: await page.getAttribute('html', 'data-theme'), firstImgSrc: darkSrc, visible: visibleDark });
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log(JSON.stringify(shots, null, 2));
|
||||
// 断言:浅色无 data-theme 属性(null),可见 logo.svg;深色 data-theme=dark,可见 logo-white.svg
|
||||
const lightOk = shots[0].visible === 'logo.svg' && shots[0].htmlDataTheme !== 'dark';
|
||||
const darkOk = shots[1].visible === 'logo-white.svg' && shots[1].htmlDataTheme === 'dark';
|
||||
console.log(`\n[assert] light shows logo.svg: ${lightOk}`);
|
||||
console.log(`[assert] dark shows logo-white.svg: ${darkOk}`);
|
||||
console.log(`[result] ${lightOk && darkOk ? 'PASS' : 'FAIL'}`);
|
||||
}
|
||||
|
||||
// 返回当前实际可见(display !== none)的 logo img 的 src
|
||||
async function getVisible(page) {
|
||||
return page.evaluate(() => {
|
||||
const imgs = Array.from(document.querySelectorAll('header img'));
|
||||
const vis = imgs.filter((i) => {
|
||||
const s = getComputedStyle(i);
|
||||
return s.display !== 'none' && s.visibility !== 'hidden' && Number(s.opacity) > 0;
|
||||
});
|
||||
return vis.length ? vis[0].getAttribute('src') : 'NONE_VISIBLE';
|
||||
});
|
||||
}
|
||||
|
||||
run().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user