Files
BoosAPI/desktop/libs/markstream-vue/test/theme-gen-regressions.test.ts
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

133 lines
4.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { contrastRatio, hslToRgb, rgbToHsl } from '../scripts/theme-gen/color.mjs'
import { generateBothSchemes, validateContrast } from '../scripts/theme-gen/dark.mjs'
interface HslColor {
h: number
s: number
l: number
}
type TokenMap = Record<string, string>
const lightSemanticKeyColors = {
info: '#1d4ed8',
success: '#166534',
warning: '#92400e',
}
const darkSemanticKeyColors = {
info: '#78a9ff',
success: '#63d29d',
warning: '#ffbe5c',
}
function parseToken(token: string): HslColor {
const values = token.match(/[\d.]+/g)?.map(Number)
if (!values || values.length < 3)
throw new Error(`Invalid token: ${token}`)
return { h: values[0], s: values[1], l: values[2] }
}
function alphaBlend(foreground: HslColor, background: HslColor, alpha: number): HslColor {
const fg = hslToRgb(foreground.h, foreground.s, foreground.l)
const bg = hslToRgb(background.h, background.s, background.l)
return rgbToHsl(
Math.round(fg.r * alpha + bg.r * (1 - alpha)),
Math.round(fg.g * alpha + bg.g * (1 - alpha)),
Math.round(fg.b * alpha + bg.b * (1 - alpha)),
)
}
function getThemeMode(tokens: TokenMap): 'light' | 'dark' {
return parseToken(tokens.background).l > 50 ? 'light' : 'dark'
}
function getActualAdmonitionHeaderBackground(tokens: TokenMap, semanticKey: 'info' | 'success' | 'warning') {
const mode = getThemeMode(tokens)
const semantic = parseToken(tokens[semanticKey])
const muted = parseToken(tokens.muted)
const alpha = mode === 'light' ? 0.06 : 0.12
return alphaBlend(semantic, muted, alpha)
}
describe('theme-gen regressions', () => {
it('keeps provided semantic header colors readable on the actual admonition header background', () => {
const cases = [
{ name: 'claude', background: '#f5f4ed', foreground: '#141413', brand: '#c96442', ...lightSemanticKeyColors },
{ name: 'green', background: '#f0fdf4', foreground: '#14532d', brand: '#16a34a', ...lightSemanticKeyColors },
{ name: 'ink', background: '#0b1020', foreground: '#e5ecff', brand: '#7c3aed', ...darkSemanticKeyColors },
]
for (const { name, ...keyColors } of cases) {
const { light, dark } = generateBothSchemes(keyColors)
let checkedPairs = 0
for (const [mode, tokens] of Object.entries({ light, dark })) {
for (const semanticKey of ['info', 'success', 'warning'] as const) {
if (!tokens[semanticKey])
continue
checkedPairs += 1
const semantic = parseToken(tokens[semanticKey])
const actualHeaderBg = getActualAdmonitionHeaderBackground(tokens, semanticKey)
expect(
contrastRatio(semantic, actualHeaderBg),
`${name} ${mode} ${semanticKey} should stay readable on the real admonition header background`,
).toBeGreaterThanOrEqual(4.5)
}
}
expect(checkedPairs, `${name} should emit semantic tokens for at least one generated scheme`).toBeGreaterThan(0)
}
})
it('reports semantic header contrast using the real admonition background composition', () => {
const { light } = generateBothSchemes({
background: '#f5f4ed',
foreground: '#141413',
brand: '#c96442',
...lightSemanticKeyColors,
})
const report = validateContrast(light)
for (const semanticKey of ['info', 'success', 'warning'] as const) {
const entry = report.find(result => result.fg === semanticKey)
const actualRatio = contrastRatio(
parseToken(light[semanticKey]),
getActualAdmonitionHeaderBackground(light, semanticKey),
)
expect(entry, `${semanticKey} should be included in the contrast report`).toBeTruthy()
expect(
Math.abs((entry?.ratio ?? 0) - actualRatio),
`${semanticKey} report should match the real admonition header composition`,
).toBeLessThan(0.15)
expect(entry?.passAA).toBe(actualRatio >= (entry?.threshold ?? 4.5))
}
})
it('finds a distinguishable dark-scheme link for blue brands when a nearby valid candidate exists', () => {
const { dark } = generateBothSchemes({
background: '#ffffff',
foreground: '#111111',
brand: '#2563eb',
})
const background = parseToken(dark.background)
const foreground = parseToken(dark.foreground)
const generatedLink = parseToken(dark.link)
const nearbyValidCandidate = { h: 228, s: 84, l: 68 }
expect(contrastRatio(nearbyValidCandidate, background)).toBeGreaterThanOrEqual(4.5)
expect(contrastRatio(nearbyValidCandidate, foreground)).toBeGreaterThanOrEqual(3)
expect(contrastRatio(generatedLink, background)).toBeGreaterThanOrEqual(4.5)
expect(
contrastRatio(generatedLink, foreground),
'generated dark link should not stay in warn territory when a nearby blue-purple candidate satisfies both constraints',
).toBeGreaterThanOrEqual(3)
})
})