Files
apes-sidebar/tests/theme.spec.ts
T
Menghuan1918 162c16ec32 feat(file-icon-theme): 可选彩色图标主题接线(懒加载 chunk + 设置开关 + i18n)
- 新 chunk `file-icons`(`lib/client-file-icons.js`,255 kB / gzip 90 kB):把数据表
  注册成 `registerFileIcon` 描述符——按 glyph+color 归并,563 条规则 → 约 250 条注册;
  目录规则只走 `folderNames`(只认领列出的目录,未列出的仍用内置 glyph)。
- `src/client/file-icon-theme.ts`:pref → loadChunk → 注册/注销,generation 守卫
  丢弃「切换中途落地」的加载;chunk 拉取失败只 log,保持内置 glyph。
- pref `fileIconTheme`(默认 `'builtin'`,未知值按 builtin 处理)+ 设置页
  「文件 → 文件图标」下拉(内置单色 / 彩色品牌图标)。
- chunk 名同步四处(chunk-loader / bundle-route / tsdown / manifest-consistency)+
  `bundle-route.spec`;tsdown 为该 chunk 加 react-icons ESM alias——否则整包打进
  6.4 MB,alias 后 255 kB。
- i18n:6 个新 key 镜像到全部 19 份词典(zh/en 之外),locale 键集守护通过。
- 皮肤契约:指南 §12 增豁免条款(品牌色是内容非 chrome,仅限该懒加载 chunk),
  AGENTS §5 同步;`tests/theme.spec.ts` 新增边界用例(核心图标模块零颜色字面量、
  核心不静态 import 该 chunk)。
- 测试:`tests/file-icon-theme.spec.tsx` 7 例(loader 六态 + 真实数据三表注册);
  prefs 新增默认值/解析用例;builtins / prefs / plugin-shape / smoke 夹具补字段。
- 全量 `pnpm test` 失败集与 main 基线逐条一致(5 文件 / 26 例预存环境失败),
  新增用例全绿;typecheck / lint / build / check:consumer-types 全通过。
2026-09-08 21:43:28 +08:00

121 lines
4.9 KiB
TypeScript
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.
/**
* Live theme-token access tests — issue #90's guard.
*
* `effectiveTokenValue` must treat visually inert values (CSS reset
* keywords, `transparent`, and any color below the opacity floor) as UNSET
* so callers' `|| fallback` chains fire. Skin systems set global tokens like
* `--dsw-alias-bg-base` to `transparent` or translucent glass values (the
* dsh-web-ui skins use rgba 0.16–0.7); without this guard a
* truthy-but-inert value would leave the terminal/editor see-through over
* the skin's backdrop. Effectively opaque values (>= 0.9 alpha, including a
* skin's scoped 0.96 porcelain) pass through so the skin still controls the
* surface.
*/
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest'
import { readFileSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { colorAlpha, effectiveTokenValue, tokenValue } from '../src/client/theme.ts'
afterEach(() => {
document.body.removeAttribute('style')
})
describe('colorAlpha', () => {
it('parses the hex family', () => {
expect(colorAlpha('#fff')).toBe(1)
expect(colorAlpha('#fff8')).toBeCloseTo(0x88 / 255)
expect(colorAlpha('#112233')).toBe(1)
expect(colorAlpha('#11223344')).toBeCloseTo(0x44 / 255)
})
it('parses rgb()/rgba() in comma and space syntax', () => {
expect(colorAlpha('rgb(1, 2, 3)')).toBe(1)
expect(colorAlpha('rgba(255, 255, 255, 0.45)')).toBeCloseTo(0.45)
expect(colorAlpha('rgb(255 255 255 / 0.7)')).toBeCloseTo(0.7)
})
it('parses hsl()/hsla()', () => {
expect(colorAlpha('hsl(200, 50%, 50%)')).toBe(1)
expect(colorAlpha('hsla(200, 50%, 50%, 0.3)')).toBeCloseTo(0.3)
})
it('treats unparseable formats as opaque', () => {
expect(colorAlpha('white')).toBeNull()
expect(colorAlpha('')).toBeNull()
})
})
describe('effectiveTokenValue', () => {
it('passes through real opaque paint values verbatim', () => {
document.body.style.setProperty('--probe', '#112233')
expect(effectiveTokenValue('--probe')).toBe('#112233')
// Effectively opaque translucency (a skin's scoped 0.96 porcelain glass)
// is a deliberate surface choice — it passes through.
document.body.style.setProperty('--probe', 'rgba(10, 22, 54, 0.96)')
expect(effectiveTokenValue('--probe')).toBe('rgba(10, 22, 54, 0.96)')
})
it('treats transparent and CSS reset keywords as unset', () => {
for (const inert of ['transparent', 'initial', 'inherit', 'unset']) {
document.body.style.setProperty('--probe', inert)
expect(effectiveTokenValue('--probe'), inert).toBe('')
}
})
it('treats translucent glass values below the opacity floor as unset', () => {
// The dsh-web-ui skins set bg-base to rgba 0.16–0.7 for glass panels;
// the terminal must fall back to an opaque background there.
for (const glass of ['rgba(255, 255, 255, 0.45)', 'rgba(20, 26, 46, 0.7)', '#11223344']) {
document.body.style.setProperty('--probe', glass)
expect(effectiveTokenValue('--probe'), glass).toBe('')
}
})
it('treats a missing token as unset', () => {
expect(effectiveTokenValue('--never-defined')).toBe('')
})
it('tokenValue still returns the raw value', () => {
document.body.style.setProperty('--probe', 'transparent')
expect(tokenValue('--probe')).toBe('transparent')
expect(effectiveTokenValue('--probe')).toBe('')
})
})
/**
* Skin contract (guide §12): every visual value rides a `--dsw-alias-*` /
* `--dsw-font-*` / `--ds-*` token. The ONE documented exception is the
* optional colored icon theme: brand colors are identity, not chrome, so
* they are hardcoded — but only inside the lazy `file-icons` chunk, which
* the core bundle must never import. This guards that boundary.
*/
// jsdom has no file:// import.meta.url; vitest runs from the repo root.
const ROOT = process.cwd()
describe('skin contract boundary for hardcoded icon colors', () => {
const coreIconModules = ['src/client/file-icons.tsx', 'src/client/file-icon-theme.ts']
it('the core icon modules carry no color literals', () => {
for (const file of coreIconModules) {
const source = readFileSync(resolve(ROOT, file), 'utf8')
expect(source, file).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
expect(source, file).not.toMatch(/\brgba?\(/)
}
})
it('only the lazy chunk may carry the colored dataset, and the core never imports it statically', () => {
const chunk = readFileSync(resolve(ROOT, 'src/client/chunks/file-icons.tsx'), 'utf8')
expect(chunk).toMatch(/#[0-9a-fA-F]{6}/)
const clientDir = resolve(ROOT, 'src/client')
const offenders: string[] = []
for (const entry of readdirSync(clientDir, { recursive: true, withFileTypes: true })) {
if (!entry.isFile() || !/\.tsx?$/.test(entry.name)) continue
const path = resolve(entry.parentPath, entry.name)
if (path.includes('/chunks/')) continue
if (/from '[^']*chunks\/file-icons/.test(readFileSync(path, 'utf8'))) offenders.push(path)
}
expect(offenders).toEqual([])
})
})