boot-theme.client.spec.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  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 { bootThemeInjections } 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. for (const row of bootThemeInjections(preference, fontSize)) {
  13. if (row.kind === 'script') runInNewContext(row.text, { document, matchMedia: globalThis.matchMedia })
  14. }
  15. }
  16. afterEach(() => {
  17. vi.restoreAllMocks()
  18. vi.unstubAllGlobals()
  19. delete document.documentElement.dataset.dsThemeSource
  20. document.body.removeAttribute(DARK_ATTRIBUTE)
  21. document.body.style.removeProperty('--dsh-content-font-size')
  22. })
  23. describe('theme bootstrap row', () => {
  24. it('colors the body with head CSS before applying body state', () => {
  25. mockSystemDark(false)
  26. const [head, body] = bootThemeInjections('dark')
  27. expect(head).toMatchObject({ kind: 'style' })
  28. expect(body).toMatchObject({ kind: 'script', placement: 'body' })
  29. if (head?.kind !== 'style') throw new Error('theme head bootstrap row is not a style')
  30. expect(head.text).toBe(':root{color-scheme:dark}body{background-color:#151517;--dsh-boot-bg:#151517}')
  31. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  32. if (body?.kind !== 'script') throw new Error('theme body bootstrap row is not a script')
  33. runInNewContext(body.text, { document, matchMedia: globalThis.matchMedia })
  34. expect(document.documentElement.dataset.dsThemeSource).toBe('dark')
  35. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
  36. })
  37. it('lets durable light override a dark OS and clears stale dark state', () => {
  38. document.body.setAttribute(DARK_ATTRIBUTE, '')
  39. mockSystemDark(true)
  40. const [head] = bootThemeInjections('light')
  41. if (head?.kind !== 'style') throw new Error('theme head bootstrap row is not a style')
  42. expect(head.text).toBe(':root{color-scheme:light}body{background-color:#fff;--dsh-boot-bg:#fff}')
  43. executeBootstrap('light')
  44. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  45. })
  46. it.each([
  47. [true, true],
  48. [false, false],
  49. ] as const)('resolves system=%s for the body palette', (matches, dark) => {
  50. mockSystemDark(matches)
  51. executeBootstrap('system')
  52. expect(document.documentElement.dataset.dsThemeSource).toBe('system')
  53. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(dark)
  54. })
  55. it('uses a media query for the system canvas palette', () => {
  56. const [head] = bootThemeInjections('system')
  57. if (head?.kind !== 'style') throw new Error('theme head bootstrap row is not a style')
  58. expect(head.text).toBe(
  59. ':root{color-scheme:light}body{background-color:#fff;--dsh-boot-bg:#fff}'
  60. + '@media(prefers-color-scheme:dark){:root{color-scheme:dark}body{background-color:#151517;--dsh-boot-bg:#151517}}',
  61. )
  62. })
  63. it('defaults to system and falls back to light when matchMedia is unavailable', () => {
  64. vi.stubGlobal('matchMedia', undefined)
  65. executeBootstrap()
  66. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  67. })
  68. it('writes the durable content font size and defaults it to 14px', () => {
  69. mockSystemDark(false)
  70. executeBootstrap('light', 17)
  71. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px')
  72. executeBootstrap('light')
  73. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px')
  74. })
  75. })