A test file under packages/client now says which face it covers:
`*.client.spec.{ts,tsx}` and its `*.client.{ts,tsx}` helpers belong to the
Client aggregate, `*.host.spec.ts` to the host aggregate. The carrier's four
node-half specs take the Host suffix.
The two suffixes are mutually exclusive, so each aggregate excludes the
other's and both keep one broad test glob: `exclude` wins over `include`, and
`packages/client/**` no longer has to be excluded wholesale from the host
program with per-file `files` entries carved back out of it. A Host-face spec
that reaches only Host source therefore needs no cross-face project
reference, which the split-project rule rejects.
vitest still discovers every file through `**/*.spec.{ts,tsx}`.
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
/** Host index injection and the resulting pre-plugin browser theme. */
|
|
import { runInNewContext } from 'node:vm'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { injectBootTheme } from '../src/boot-theme.ts'
|
|
import type { ThemePreference } from '../src/theme-settings.ts'
|
|
|
|
const DARK_ATTRIBUTE = 'data-ds-dark-theme'
|
|
|
|
function mockSystemDark(matches: boolean): void {
|
|
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches }) as MediaQueryList))
|
|
}
|
|
|
|
function executeBootstrap(
|
|
preference?: ThemePreference,
|
|
html = '<html><body><div id="root"></div><script type="module"></script></body></html>',
|
|
): string {
|
|
const injected = injectBootTheme(html, preference)
|
|
const source = /<script>([\s\S]*?)<\/script>/.exec(injected)?.[1]
|
|
if (source === undefined) throw new Error('theme bootstrap script missing')
|
|
runInNewContext(source, { document, matchMedia: globalThis.matchMedia })
|
|
return injected
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
vi.unstubAllGlobals()
|
|
document.documentElement.style.removeProperty('color-scheme')
|
|
document.body.removeAttribute(DARK_ATTRIBUTE)
|
|
})
|
|
|
|
describe('theme boot index transform', () => {
|
|
it('runs immediately inside the body before the shell mount', () => {
|
|
mockSystemDark(false)
|
|
const html = executeBootstrap('dark', '<html><body class="app"><div id="root"></div></body></html>')
|
|
expect(html.indexOf('<script>')).toBeGreaterThan(html.indexOf('<body class="app">'))
|
|
expect(html.indexOf('<script>')).toBeLessThan(html.indexOf('<div id="root">'))
|
|
expect(document.documentElement.style.colorScheme).toBe('dark')
|
|
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
|
|
})
|
|
|
|
it('lets durable light override a dark OS and clears stale dark state', () => {
|
|
document.body.setAttribute(DARK_ATTRIBUTE, '')
|
|
mockSystemDark(true)
|
|
executeBootstrap('light')
|
|
expect(document.documentElement.style.colorScheme).toBe('light')
|
|
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
[true, 'dark', true],
|
|
[false, 'light', false],
|
|
] as const)('resolves system=%s to %s', (matches, colorScheme, dark) => {
|
|
mockSystemDark(matches)
|
|
executeBootstrap('system')
|
|
expect(document.documentElement.style.colorScheme).toBe(colorScheme)
|
|
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(dark)
|
|
})
|
|
|
|
it('defaults to system and falls back to light when matchMedia is unavailable', () => {
|
|
vi.stubGlobal('matchMedia', undefined)
|
|
executeBootstrap()
|
|
expect(document.documentElement.style.colorScheme).toBe('light')
|
|
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
|
|
})
|
|
|
|
it('appends the script to a body-less fragment', () => {
|
|
const html = injectBootTheme('<main>loading</main>', 'dark')
|
|
expect(html.startsWith('<main>loading</main><script>')).toBe(true)
|
|
})
|
|
})
|