boot-theme.spec.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. // @vitest-environment jsdom
  2. /** Host index injection and the resulting pre-plugin browser theme. */
  3. import { runInNewContext } from 'node:vm'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import { injectBootTheme } 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(
  12. preference?: ThemePreference,
  13. html = '<html><body><div id="root"></div><script type="module"></script></body></html>',
  14. ): string {
  15. const injected = injectBootTheme(html, preference)
  16. const source = /<script>([\s\S]*?)<\/script>/.exec(injected)?.[1]
  17. if (source === undefined) throw new Error('theme bootstrap script missing')
  18. runInNewContext(source, { document, matchMedia: globalThis.matchMedia })
  19. return injected
  20. }
  21. afterEach(() => {
  22. vi.restoreAllMocks()
  23. vi.unstubAllGlobals()
  24. document.documentElement.style.removeProperty('color-scheme')
  25. document.body.removeAttribute(DARK_ATTRIBUTE)
  26. })
  27. describe('theme boot index transform', () => {
  28. it('runs immediately inside the body before the shell mount', () => {
  29. mockSystemDark(false)
  30. const html = executeBootstrap('dark', '<html><body class="app"><div id="root"></div></body></html>')
  31. expect(html.indexOf('<script>')).toBeGreaterThan(html.indexOf('<body class="app">'))
  32. expect(html.indexOf('<script>')).toBeLessThan(html.indexOf('<div id="root">'))
  33. expect(document.documentElement.style.colorScheme).toBe('dark')
  34. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
  35. })
  36. it('lets durable light override a dark OS and clears stale dark state', () => {
  37. document.body.setAttribute(DARK_ATTRIBUTE, '')
  38. mockSystemDark(true)
  39. executeBootstrap('light')
  40. expect(document.documentElement.style.colorScheme).toBe('light')
  41. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  42. })
  43. it.each([
  44. [true, 'dark', true],
  45. [false, 'light', false],
  46. ] as const)('resolves system=%s to %s', (matches, colorScheme, dark) => {
  47. mockSystemDark(matches)
  48. executeBootstrap('system')
  49. expect(document.documentElement.style.colorScheme).toBe(colorScheme)
  50. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(dark)
  51. })
  52. it('defaults to system and falls back to light when matchMedia is unavailable', () => {
  53. vi.stubGlobal('matchMedia', undefined)
  54. executeBootstrap()
  55. expect(document.documentElement.style.colorScheme).toBe('light')
  56. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  57. })
  58. it('appends the script to a body-less fragment', () => {
  59. const html = injectBootTheme('<main>loading</main>', 'dark')
  60. expect(html.startsWith('<main>loading</main><script>')).toBe(true)
  61. })
  62. })