1- import { renderHook , waitFor } from '@testing-library/react' ;
1+ import { act , cleanup , renderHook , screen , waitFor } from '@testing-library/react' ;
2+ import { createElement , type PropsWithChildren } from 'react' ;
3+
4+ import { BaseStyles } from '@primer/react' ;
5+ import { ThemeProvider , useTheme } from '@primer/react/next' ;
26
37import { useSettingsStore } from '../stores' ;
48
@@ -13,9 +17,9 @@ const primerTheme = vi.hoisted(() => ({
1317 setNightScheme : vi . fn ( ) ,
1418} ) ) ;
1519
16- vi . mock ( '@primer/react' , async ( importOriginal ) => ( {
17- ...( await importOriginal < typeof import ( '@primer/react' ) > ( ) ) ,
18- useTheme : ( ) => primerTheme ,
20+ vi . mock ( '@primer/react/next ' , async ( importOriginal ) => ( {
21+ ...( await importOriginal < typeof import ( '@primer/react/next ' ) > ( ) ) ,
22+ useTheme : vi . fn ( ( ) => primerTheme ) ,
1923} ) ) ;
2024
2125function mockPrefersContrast ( matches : boolean ) {
@@ -30,6 +34,10 @@ function mockPrefersContrast(matches: boolean) {
3034}
3135
3236describe ( 'renderer/hooks/useAppearance.ts' , ( ) => {
37+ beforeEach ( ( ) => {
38+ vi . mocked ( useTheme ) . mockImplementation ( ( ) => primerTheme ) ;
39+ } ) ;
40+
3341 afterEach ( ( ) => {
3442 document . documentElement . removeAttribute ( 'data-theme' ) ;
3543 document . documentElement . removeAttribute ( 'data-glass-material' ) ;
@@ -211,4 +219,113 @@ describe('renderer/hooks/useAppearance.ts', () => {
211219
212220 expect ( document . documentElement . classList . contains ( 'gitify-colored-icons' ) ) . toBe ( false ) ;
213221 } ) ;
222+
223+ describe ( 'with ThemeProvider' , ( ) => {
224+ let darkMedia : EventTarget & { matches : boolean } ;
225+
226+ beforeEach ( async ( ) => {
227+ const actual =
228+ await vi . importActual < typeof import ( '@primer/react/next' ) > ( '@primer/react/next' ) ;
229+ vi . mocked ( useTheme ) . mockImplementation ( actual . useTheme ) ;
230+
231+ darkMedia = Object . assign ( new EventTarget ( ) , { matches : false } ) ;
232+ const otherMedia = Object . assign ( new EventTarget ( ) , { matches : false } ) ;
233+ vi . spyOn ( window , 'matchMedia' ) . mockImplementation (
234+ ( query ) =>
235+ ( query === '(prefers-color-scheme: dark)' ? darkMedia : otherMedia ) as MediaQueryList ,
236+ ) ;
237+ } ) ;
238+
239+ afterEach ( ( ) => {
240+ cleanup ( ) ;
241+ vi . restoreAllMocks ( ) ;
242+ vi . mocked ( useTheme ) . mockImplementation ( ( ) => primerTheme ) ;
243+ } ) ;
244+
245+ function renderWithTheme ( theme : Theme ) {
246+ useSettingsStore . setState ( { theme } ) ;
247+
248+ return renderHook (
249+ ( ) => {
250+ useAppearance ( ) ;
251+ return useTheme ( ) ;
252+ } ,
253+ {
254+ wrapper : ( { children } : PropsWithChildren ) =>
255+ createElement (
256+ ThemeProvider ,
257+ null ,
258+ createElement (
259+ BaseStyles ,
260+ null ,
261+ createElement ( 'div' , { 'data-testid' : 'theme-content' } , children ) ,
262+ ) ,
263+ ) ,
264+ } ,
265+ ) ;
266+ }
267+
268+ function getThemeWrapper ( ) {
269+ const wrapper = screen . getByTestId ( 'theme-content' ) . closest ( '[data-color-mode]' ) ;
270+ expect ( wrapper ) . toBeInTheDocument ( ) ;
271+ return wrapper ! ;
272+ }
273+
274+ it ( 'applies appearance to the real provider and preserves its descendant wrapper' , ( ) => {
275+ const { result } = renderWithTheme ( Theme . LIGHT ) ;
276+
277+ expect ( result . current . colorMode ) . toBe ( 'day' ) ;
278+ expect ( result . current . colorScheme ) . toBe ( 'light' ) ;
279+ const wrapper = getThemeWrapper ( ) ;
280+ expect ( wrapper ) . not . toBe ( document . documentElement ) ;
281+ expect ( wrapper ) . toHaveAttribute ( 'data-color-mode' , 'light' ) ;
282+ expect ( wrapper ) . toHaveAttribute ( 'data-light-theme' , 'light' ) ;
283+ expect ( wrapper ) . toHaveAttribute ( 'data-dark-theme' , 'light' ) ;
284+ expect ( wrapper . querySelector ( '[data-component="BaseStyles"]' ) ) . toContainElement (
285+ screen . getByTestId ( 'theme-content' ) ,
286+ ) ;
287+ } ) ;
288+
289+ it ( 'updates the real provider when settings change from light to dark' , async ( ) => {
290+ const { result } = renderWithTheme ( Theme . LIGHT ) ;
291+
292+ await act ( async ( ) => {
293+ useSettingsStore . setState ( { theme : Theme . DARK } ) ;
294+ } ) ;
295+
296+ expect ( result . current . colorMode ) . toBe ( 'night' ) ;
297+ expect ( result . current . colorScheme ) . toBe ( 'dark' ) ;
298+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-color-mode' , 'dark' ) ;
299+ expect ( window . gitify . setNativeTheme ) . toHaveBeenLastCalledWith ( 'dark' ) ;
300+ } ) ;
301+
302+ it ( 'follows OS changes in System mode without changing the saved preference' , async ( ) => {
303+ const { result } = renderWithTheme ( Theme . SYSTEM ) ;
304+ expect ( result . current . resolvedColorMode ) . toBe ( 'day' ) ;
305+
306+ await act ( async ( ) => {
307+ darkMedia . matches = true ;
308+ darkMedia . dispatchEvent ( new Event ( 'change' ) ) ;
309+ } ) ;
310+
311+ expect ( result . current . colorMode ) . toBe ( 'auto' ) ;
312+ expect ( result . current . resolvedColorMode ) . toBe ( 'night' ) ;
313+ expect ( result . current . colorScheme ) . toBe ( 'dark' ) ;
314+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-color-mode' , 'auto' ) ;
315+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-light-theme' , 'light' ) ;
316+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-dark-theme' , 'dark' ) ;
317+ expect ( useSettingsStore . getState ( ) . theme ) . toBe ( Theme . SYSTEM ) ;
318+ expect ( window . gitify . setNativeTheme ) . toHaveBeenLastCalledWith ( 'system' ) ;
319+ expect ( window . gitify . setNativeTheme ) . not . toHaveBeenCalledWith ( 'dark' ) ;
320+ } ) ;
321+
322+ it ( 'applies an accessibility scheme through the real provider' , ( ) => {
323+ const { result } = renderWithTheme ( Theme . DARK_COLORBLIND ) ;
324+
325+ expect ( result . current . colorMode ) . toBe ( 'night' ) ;
326+ expect ( result . current . colorScheme ) . toBe ( 'dark_colorblind' ) ;
327+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-color-mode' , 'dark' ) ;
328+ expect ( getThemeWrapper ( ) ) . toHaveAttribute ( 'data-dark-theme' , 'dark_colorblind' ) ;
329+ } ) ;
330+ } ) ;
214331} ) ;
0 commit comments