theme.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { flushPromises } from '@vue/test-utils'
  3. import { createPinia, setActivePinia, storeToRefs } from 'pinia'
  4. import {
  5. addEventListener,
  6. mockMediaTheme,
  7. } from '#tests/support/mock-mediaTheme.ts'
  8. import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
  9. import { EnumAppearanceTheme } from '#shared/graphql/types.ts'
  10. import { mockUserCurrentAppearanceMutation } from '#desktop/pages/personal-setting/graphql/mutations/userCurrentAppearance.mocks.ts'
  11. import { useThemeStore } from '#desktop/stores/theme.ts'
  12. const mockUserTheme = (theme: string | undefined) => {
  13. mockUserCurrent({
  14. preferences: {
  15. theme,
  16. },
  17. })
  18. }
  19. const getRoot = () => document.querySelector(':root') as HTMLElement
  20. const haveDOMTheme = (theme: string | undefined) => {
  21. const root = getRoot()
  22. if (!theme) {
  23. root.removeAttribute('data-theme')
  24. root.style.colorScheme = 'normal'
  25. } else {
  26. root.dataset.theme = theme
  27. root.style.colorScheme = theme
  28. }
  29. }
  30. const getDOMTheme = () => getRoot().dataset.theme
  31. const getDOMColorScheme = () => getRoot().style.colorScheme
  32. describe('useThemeStore', () => {
  33. beforeEach(() => {
  34. setActivePinia(createPinia())
  35. mockUserCurrent({
  36. lastname: 'Doe',
  37. firstname: 'John',
  38. preferences: {},
  39. })
  40. const { syncTheme } = useThemeStore()
  41. syncTheme()
  42. haveDOMTheme(undefined)
  43. mockMediaTheme(EnumAppearanceTheme.Light)
  44. })
  45. it('should fallback to auto when no theme present', () => {
  46. const { currentTheme } = useThemeStore()
  47. expect(currentTheme).toBe(EnumAppearanceTheme.Auto)
  48. })
  49. it('changes app theme', async () => {
  50. const mockerUserCurrentAppearanceUpdate = mockUserCurrentAppearanceMutation(
  51. {
  52. userCurrentAppearance: {
  53. success: true,
  54. },
  55. },
  56. )
  57. const themeStore = useThemeStore()
  58. const { updateTheme } = themeStore
  59. const { currentTheme, savingTheme } = storeToRefs(themeStore)
  60. await updateTheme(EnumAppearanceTheme.Dark)
  61. expect(currentTheme.value).toBe(EnumAppearanceTheme.Dark)
  62. const mockCalls = await mockerUserCurrentAppearanceUpdate.waitForCalls()
  63. expect(mockCalls).toHaveLength(1)
  64. await flushPromises()
  65. expect(savingTheme.value).toBe(false)
  66. expect(currentTheme.value).toBe(EnumAppearanceTheme.Dark)
  67. })
  68. it('should change theme value back to old value when update fails', async () => {
  69. const mockerUserCurrentAppearanceUpdate = mockUserCurrentAppearanceMutation(
  70. {
  71. userCurrentAppearance: {
  72. errors: [
  73. {
  74. message: 'Failed to update.',
  75. },
  76. ],
  77. },
  78. },
  79. )
  80. const themeStore = useThemeStore()
  81. const { updateTheme } = themeStore
  82. const { currentTheme, savingTheme } = storeToRefs(themeStore)
  83. await updateTheme(EnumAppearanceTheme.Dark)
  84. expect(currentTheme.value).toBe(EnumAppearanceTheme.Auto)
  85. const mockCalls = await mockerUserCurrentAppearanceUpdate.waitForCalls()
  86. expect(mockCalls).toHaveLength(1)
  87. await flushPromises()
  88. expect(savingTheme.value).toBe(false)
  89. expect(currentTheme.value).toBe(EnumAppearanceTheme.Auto)
  90. })
  91. it('when user has no theme preference, takes from media', () => {
  92. const { syncTheme } = useThemeStore()
  93. syncTheme()
  94. const { currentTheme } = useThemeStore()
  95. expect(currentTheme).toBe('auto')
  96. })
  97. it("changes in media don't affect theme", async () => {
  98. mockUserTheme(EnumAppearanceTheme.Dark)
  99. mockMediaTheme(EnumAppearanceTheme.Dark)
  100. const { syncTheme, currentTheme } = useThemeStore()
  101. syncTheme()
  102. expect(currentTheme).toBe(EnumAppearanceTheme.Dark)
  103. expect(getDOMTheme()).toBe(EnumAppearanceTheme.Dark)
  104. expect(getDOMColorScheme()).toBe(EnumAppearanceTheme.Dark)
  105. mockMediaTheme(EnumAppearanceTheme.Light)
  106. addEventListener.mock.calls[0][1]()
  107. expect(currentTheme).toBe(EnumAppearanceTheme.Dark)
  108. expect(getDOMTheme()).toBe(EnumAppearanceTheme.Dark)
  109. expect(getDOMColorScheme()).toBe(EnumAppearanceTheme.Dark)
  110. })
  111. describe('isDarkMode', () => {
  112. it('returns true when user prefers dark media theme', async () => {
  113. mockMediaTheme(EnumAppearanceTheme.Dark)
  114. const { isDarkMode } = useThemeStore()
  115. expect(isDarkMode).toBe(true)
  116. })
  117. it('returns false when user prefers light media theme', async () => {
  118. mockMediaTheme(EnumAppearanceTheme.Light)
  119. const { isDarkMode } = useThemeStore()
  120. expect(isDarkMode).toBe(false)
  121. })
  122. it('returns true when user has dark theme active', async () => {
  123. mockUserTheme(EnumAppearanceTheme.Dark) // has precedence
  124. mockMediaTheme(EnumAppearanceTheme.Light)
  125. const { isDarkMode } = useThemeStore()
  126. expect(isDarkMode).toBe(true)
  127. })
  128. it('returns false when user prefers light media theme', async () => {
  129. mockUserTheme(EnumAppearanceTheme.Light) // has precedence
  130. mockMediaTheme(EnumAppearanceTheme.Dark)
  131. const { isDarkMode } = useThemeStore()
  132. expect(isDarkMode).toBe(false)
  133. })
  134. })
  135. })