theme-presenter.client.spec.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // @vitest-environment jsdom
  2. import { afterEach, beforeEach, describe, expect, it } from 'vitest'
  3. import type { ThemePreference, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
  4. import { DARK_ATTRIBUTE, THEME_SOURCE_ATTRIBUTE, ThemePresenter } from '@deepseek-ai/dsh-client-ui-layout/src/client/theme-presenter.ts'
  5. const LIGHT_THEME_COLOR = 'rgb(255, 255, 255)'
  6. const DARK_THEME_COLOR = 'rgb(21, 21, 23)'
  7. function snapshot(
  8. colorScheme: 'light' | 'dark',
  9. tokens: Record<string, string> = {},
  10. fontSize = 14,
  11. preference: ThemePreference = colorScheme,
  12. ): ThemeSnapshot {
  13. // The presenter must key off colorScheme, not the id — keep them distinct.
  14. const active = { id: `${colorScheme}-test`, colorScheme, tokens }
  15. return { preference, fontSize, active, themes: [active], revision: 1 }
  16. }
  17. function clearThemePresentation(): void {
  18. document.head.querySelectorAll('meta[name="theme-color"], style[data-theme-presenter-test]').forEach((node) => { node.remove() })
  19. }
  20. function themeColorMeta(): HTMLMetaElement | null {
  21. return document.head.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
  22. }
  23. beforeEach(() => {
  24. clearThemePresentation()
  25. document.documentElement.style.removeProperty('color-scheme')
  26. document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE)
  27. document.body.removeAttribute(DARK_ATTRIBUTE)
  28. document.body.removeAttribute('style')
  29. const style = document.createElement('style')
  30. style.dataset.themePresenterTest = ''
  31. style.textContent = `
  32. body { background-color: ${LIGHT_THEME_COLOR}; }
  33. body[${DARK_ATTRIBUTE}] { background-color: ${DARK_THEME_COLOR}; }
  34. `
  35. document.head.append(style)
  36. })
  37. afterEach(clearThemePresentation)
  38. describe('ThemePresenter', () => {
  39. it('light scheme sets root color-scheme and leaves the dark attribute absent', () => {
  40. const presenter = new ThemePresenter()
  41. presenter.apply(snapshot('light'))
  42. expect(document.documentElement.style.colorScheme).toBe('light')
  43. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  44. expect(themeColorMeta()?.content).toBe(LIGHT_THEME_COLOR)
  45. })
  46. it('dark scheme sets root color-scheme, the attribute, and metadata; switching to light updates one node', () => {
  47. const presenter = new ThemePresenter()
  48. presenter.apply(snapshot('dark'))
  49. const meta = themeColorMeta()
  50. expect(document.documentElement.style.colorScheme).toBe('dark')
  51. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
  52. expect(meta?.content).toBe(DARK_THEME_COLOR)
  53. presenter.apply(snapshot('light'))
  54. expect(document.documentElement.style.colorScheme).toBe('light')
  55. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  56. expect(themeColorMeta()).toBe(meta)
  57. expect(meta?.content).toBe(LIGHT_THEME_COLOR)
  58. expect(document.head.querySelectorAll('meta[name="theme-color"]')).toHaveLength(1)
  59. })
  60. it('applies tokens as inline variables and clears the previous set on theme change', () => {
  61. const presenter = new ThemePresenter()
  62. presenter.apply(snapshot('dark', { '--dsw-alias-bg': '#111', '--dsw-alias-fg': '#eee' }))
  63. expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('#111')
  64. expect(document.body.style.getPropertyValue('--dsw-alias-fg')).toBe('#eee')
  65. presenter.apply(snapshot('light', { '--dsw-alias-bg': '#fff' }))
  66. expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('#fff')
  67. // The old theme's extra variable is gone, not merged.
  68. expect(document.body.style.getPropertyValue('--dsw-alias-fg')).toBe('')
  69. })
  70. it('publishes the content font size and follows changes', () => {
  71. const presenter = new ThemePresenter()
  72. presenter.apply(snapshot('light'))
  73. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px')
  74. presenter.apply(snapshot('light', {}, 17))
  75. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px')
  76. })
  77. it('publishes the theme source: system stays system, fixed preferences publish the resolved scheme', () => {
  78. const presenter = new ThemePresenter()
  79. presenter.apply(snapshot('dark', {}, 14, 'system'))
  80. expect(document.documentElement.getAttribute(THEME_SOURCE_ATTRIBUTE)).toBe('system')
  81. presenter.apply(snapshot('dark'))
  82. expect(document.documentElement.getAttribute(THEME_SOURCE_ATTRIBUTE)).toBe('dark')
  83. presenter.dispose()
  84. expect(document.documentElement.hasAttribute(THEME_SOURCE_ATTRIBUTE)).toBe(false)
  85. })
  86. it('dispose removes color-scheme, the attribute, the font-size axis, and every applied variable, sparing foreign inline styles', () => {
  87. document.body.style.setProperty('--foreign', 'kept')
  88. const presenter = new ThemePresenter()
  89. presenter.apply(snapshot('dark', { '--dsw-alias-bg': '#111' }))
  90. const meta = themeColorMeta()
  91. presenter.dispose()
  92. expect(document.documentElement.style.colorScheme).toBe('')
  93. expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
  94. expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('')
  95. expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('')
  96. expect(document.body.style.getPropertyValue('--foreign')).toBe('kept')
  97. expect(meta?.isConnected).toBe(false)
  98. })
  99. })