Files
deepseek-harness/packages/client/ui-models/tests/styles.spec.ts
Yichen Jiang dc7510a902 fix(ui-models): close the media block that swallowed the fetch dialog's styles
The `@media (prefers-reduced-motion: reduce)` block never closed, so every
rule after it — the whole fetch dialog, its candidate list, and the rows
inside — applied only to viewers whose system asks for reduced motion. The
sheet still parsed and the classes still attached, so the list painted with
the browser's own bullets, indentation, and inline label, and the reported
model id ran straight into the capacity beside it.

Report the id alone: it is the string adoption writes, and the capacities the
endpoint disclosed are adopted with it and editable in the row that appears.
The candidate row's remaining font name is the one the theme declares.

The styles gate now checks that the sheet's braces balance, and reads every
theme sheet rather than the platform tokens alone, so a name declared in a
sibling is not called undeclared and a `--dsh-` typo cannot pass as a token.
2026-08-06 15:18:17 +08:00

70 lines
3.8 KiB
TypeScript

/**
* Models section stylesheet contract, asserted against the CSS text on disk.
*
* The section paints in both themes, and a `--dsw-*` name the theme does not
* declare fails silently: the browser takes the `var()` fallback, so the sheet
* still renders and only the dark theme looks wrong. Checking the names against
* the sheet that declares them is what turns that into a test failure.
*/
import { readdirSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const css = readFileSync(fileURLToPath(new URL('../src/client/ModelsSection.module.css', import.meta.url)), 'utf8')
// The theme package maps `./styles/*` to `./src/styles/*`, so the declarations
// stay on the source plane rather than needing a build.
// Every theme sheet, not just the platform tokens: font and scrollbar
// variables are declared in siblings, and a gate reading one file would call
// their names undeclared.
const tokens = readdirSync(fileURLToPath(new URL('../../ui-theme/src/styles/', import.meta.url)))
.filter(name => name.endsWith('.css'))
.map(name => readFileSync(fileURLToPath(new URL(`../../ui-theme/src/styles/${name}`, import.meta.url)), 'utf8'))
.join('\n')
/** The declarations of one top-level rule, by selector. */
function block(selector: string): string {
const match = new RegExp(`^\\${selector} \\{([^}]*)\\}`, 'm').exec(css)
if (match === null) throw new Error(`ModelsSection.module.css has no \`${selector}\` rule`)
return match[1] ?? ''
}
describe('ModelsSection theme styles', () => {
it('names only theme variables the token sheet defines', () => {
// A `--dsw-*` name the sheet never declares is not a near miss: it silently
// resolves to whatever literal sits in its fallback slot, which is how this
// section stayed light under the dark theme before. Undeclared names have
// no fallback at all and inherit, so both spellings must fail here.
// Every theme-variable prefix the sheets actually use, not just `--dsw-`:
// a `--dsh-` name reads as a plausible sibling and would otherwise slip
// past this gate into a fallback literal.
const named = [...css.matchAll(/var\((--(?:dsw|dsh|ds)-[a-z0-9-]+)/g)].map(match => match[1])
const undeclared = [...new Set(named)].filter(name => !tokens.includes(` ${String(name)}:`))
expect(undeclared).toEqual([])
expect(css).not.toMatch(/var\(--(?:surface|text-|border|accent-strong)/)
})
it('closes every block, so no rule is swallowed by the one above it', () => {
// A missing `}` on an `@media` block is not a parse error: every rule after
// it silently becomes conditional, and the whole fetch dialog once painted
// unstyled for anyone whose system does not ask for reduced motion. Nothing
// downstream reports this — the sheet loads and the classes still attach.
const bare = css.replace(/\/\*[\s\S]*?\*\//g, '')
expect((bare.match(/\}/g) ?? []).length).toBe((bare.match(/\{/g) ?? []).length)
})
it('separates the row card from the editor it expands into', () => {
// `bg-layer-3` and `bg-module-platform` both resolve to neutral-bluish-800
// under the dark theme, so filling the row with either erases the nested
// editor's boundary. The row is outlined; the fill is the editor's alone.
expect(block('.editor')).toContain('background: var(--dsw-alias-bg-module-platform)')
expect(block('.rowCard')).toContain('border: 1px solid var(--dsw-alias-border-l2)')
expect(block('.rowCard')).not.toMatch(/\bbackground\s*:/)
})
it('never falls back to a literal colour', () => {
// A token that resolves is never the problem; an undeclared one takes this
// branch, and a literal here is a single colour for both themes.
expect(css).not.toMatch(/var\(--dsw-[a-z0-9-]+\s*,\s*(?:#|rgb|rgba|hsl|hsla)/)
})
})