Files
session-notify/tests/unit/styles.test.js
T

82 lines
5.2 KiB
JavaScript

// The stylesheet's two contracts, both of which the popup broke once:
//
// 1. it may only paint with theme tokens — a literal color cannot follow the
// shell's light/dark switch, and a `--dsw-static-*` token is fixed by design;
// 2. the popup must use the same floating-surface recipe as the shipped menus and
// modals (surface + hairline + elevation + label tokens), not the fixed dark
// `--dsw-alias-toast-bg` that made it a black slab on a light page.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { CSS } from '../../src/client/ui/styles.js'
/** Every `property:value` declaration in the sheet, selector-less. */
function declarations(sheet) {
return [...sheet.matchAll(/([a-z-]+)\s*:\s*([^;{}]+);/g)].map((match) => ({ property: match[1], value: match[2].trim() }))
}
const COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch|lab)\(|\b(?:white|black|red|green|blue|gray|grey)\b/
test('the stylesheet paints with tokens and never with a literal color', () => {
const properties = ['color', 'background', 'background-color', 'border', 'border-top', 'border-color', 'box-shadow', 'outline']
const painted = declarations(CSS).filter(({ property }) => properties.includes(property))
assert.ok(painted.length > 20, 'expected the sheet to carry its painted declarations')
for (const { property, value } of painted) {
// Keyword values carry no color of their own.
if (/^(none|transparent|inherit|currentColor)$/.test(value)) continue
assert.ok(!COLOR_LITERAL.test(value), `${property}: ${value} hardcodes a color instead of using a theme token`)
assert.ok(!value.includes('--dsw-static-'), `${property}: ${value} uses a theme-fixed static token that cannot follow dark mode`)
}
})
test('the popup is the shipped floating-card recipe, in both themes', () => {
const toast = CSS.match(/\.dsn-toast\{([^}]*)\}/)?.[1]
assert.ok(toast !== undefined, 'the popup rule is missing')
// The surface, the hairline and the shadow are what DSH's menus and modals use.
assert.ok(toast.includes('background:var(--dsw-alias-bg-layer-2)'), 'the popup must sit on the elevated layer surface')
assert.ok(toast.includes('--dsw-elevation-stroke-color:var(--dsw-alias-border-l1)'), 'the popup must take the menu hairline')
assert.ok(toast.includes('box-shadow:var(--dsw-elevation-prominent)'), 'the popup must take the prominent elevation')
assert.ok(toast.includes('color:var(--dsw-alias-label-primary)'), 'the popup text must be the primary label token')
assert.ok(toast.includes('border-radius:var(--dsw-radius-lg)'), 'the popup must take the large radius')
assert.ok(toast.includes('var(--ds-transition-duration') && toast.includes('var(--ds-ease-in-out'), 'the entrance must use the shell motion tokens')
// The one thing it must not do: the fixed dark toast slab, or any of its labels.
assert.ok(!toast.includes('--dsw-alias-toast-'), 'the popup must not use the theme-fixed toast surface')
assert.ok(!CSS.includes('--dsw-alias-toast-'), 'no rule may fall back to the fixed toast surface')
})
test('the popup reads as one card: title, body and both controls use label tokens', () => {
const rule = (name) => CSS.match(new RegExp(`\\.${name}\\{([^}]*)\\}`))?.[1] ?? ''
assert.ok(rule('dsn-toast-title').includes('var(--dsw-alias-label-primary)'))
assert.ok(rule('dsn-toast-desc').includes('var(--dsw-alias-label-secondary)'))
assert.ok(rule('dsn-toast-close').includes('color:var(--dsw-alias-label-tertiary)'))
assert.ok(rule('dsn-toast-action').includes('var(--dsw-alias-state-business-primary)'))
// Hover feedback comes from the shared interactive token, not a hand-rolled color.
assert.ok(rule('dsn-toast-action:hover').includes('var(--dsw-alias-interactive-bg-hover)'))
assert.ok(rule('dsn-toast-close:hover').includes('var(--dsw-alias-interactive-bg-hover)'))
})
test('the three popup glyphs stay distinguishable through state tokens', () => {
assert.ok(CSS.includes('.dsn-toast-icon{') && CSS.includes('var(--dsw-alias-state-warn-label)'))
assert.ok(CSS.includes('.dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)}'))
assert.ok(CSS.includes('.dsn-toast-icon.is-question{color:var(--dsw-alias-state-business-primary)}'))
})
test('every class the components render has a rule, so nothing falls back to defaults', () => {
// Guards the redesign: a renamed class that the sheet no longer styles would
// render as an unstyled block instead of failing loudly.
const expected = [
'dsn-stack', 'dsn-toast', 'dsn-toast-icon', 'dsn-toast-icon is-completion', 'dsn-toast-icon is-question',
'dsn-toast-text', 'dsn-toast-title', 'dsn-toast-desc', 'dsn-toast-actions', 'dsn-toast-action', 'dsn-toast-close',
'dsn-page', 'dsn-lede', 'dsn-group', 'dsn-group-title', 'dsn-group-note', 'dsn-list', 'dsn-row', 'dsn-row-text',
'dsn-row-title', 'dsn-row-sub', 'dsn-state', 'dsn-dot', 'dsn-facts', 'dsn-fact-key', 'dsn-fact', 'dsn-fact-value',
'dsn-fact-title', 'dsn-switch', 'dsn-thumb', 'dsn-button',
]
for (const className of expected) {
const selector = `.${className.split(' ').join('.')}`
assert.ok(CSS.includes(`${selector}{`) || CSS.includes(`${selector}[`) || CSS.includes(`${selector}:`) || CSS.includes(`${selector}.`), `no rule for ${selector}`)
}
})