boot-theme.client.spec.ts 3.5 KB

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