boot-theme.client.spec.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. // @vitest-environment jsdom
  2. /** The theme bootstrap injection row and the resulting pre-plugin browser theme. */
  3. import { runInNewContext } from 'node:vm'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import { bootThemeInjection } from '../src/boot-theme.ts'
  6. import type { ThemePreference } from '../src/theme-settings.ts'
  7. const DARK_ATTRIBUTE = 'data-ds-dark-theme'
  8. function mockSystemDark(matches: boolean): void {
  9. vi.stubGlobal('matchMedia', vi.fn(() => ({ matches }) as MediaQueryList))
  10. }
  11. function executeBootstrap(preference?: ThemePreference, fontSize?: number): void {
  12. const row = bootThemeInjection(preference, fontSize)
  13. if (row.kind !== 'script') throw new Error('theme bootstrap row is not a script')
  14. runInNewContext(row.text, { document, matchMedia: globalThis.matchMedia })
  15. }
  16. afterEach(() => {
  17. vi.restoreAllMocks()
  18. vi.unstubAllGlobals()
  19. document.documentElement.style.removeProperty('color-scheme')
  20. document.body.removeAttribute(DARK_ATTRIBUTE)
  21. document.body.style.removeProperty('--dsh-content-font-size')
  22. })
  23. describe('theme bootstrap row', () => {
  24. it('is a body script row, so it runs before the shell mount', () => {
  25. mockSystemDark(false)
  26. const row = bootThemeInjection('dark')
  27. expect(row).toMatchObject({ kind: 'script', placement: 'body' })
  28. executeBootstrap('dark')
  29. expect(document.documentElement.style.colorScheme).toBe('dark')
  30. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
  31. })
  32. it('lets durable light override a dark OS and clears stale dark state', () => {
  33. document.body.setAttribute(DARK_ATTRIBUTE, '')
  34. mockSystemDark(true)
  35. executeBootstrap('light')
  36. expect(document.documentElement.style.colorScheme).toBe('light')
  37. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  38. })
  39. it.each([
  40. [true, 'dark', true],
  41. [false, 'light', false],
  42. ] as const)('resolves system=%s to %s', (matches, colorScheme, dark) => {
  43. mockSystemDark(matches)
  44. executeBootstrap('system')
  45. expect(document.documentElement.style.colorScheme).toBe(colorScheme)
  46. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(dark)
  47. })
  48. it('defaults to system and falls back to light when matchMedia is unavailable', () => {
  49. vi.stubGlobal('matchMedia', undefined)
  50. executeBootstrap()
  51. expect(document.documentElement.style.colorScheme).toBe('light')
  52. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  53. })
  54. it('writes the durable content font size and defaults it to 14px', () => {
  55. mockSystemDark(false)
  56. executeBootstrap('light', 17)
  57. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px')
  58. executeBootstrap('light')
  59. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px')
  60. })
  61. })