@@ -12,6 +12,8 @@ import {
1212 mock ,
1313} from 'bun:test'
1414
15+ import { createThemeStore } from '../theme-store'
16+
1517beforeAll ( ( ) => {
1618 document . documentElement . removeAttribute ( 'data-theme' )
1719} )
@@ -20,136 +22,163 @@ afterAll(() => {
2022 document . documentElement . removeAttribute ( 'data-theme' )
2123} )
2224
23- describe ( 'themeStore' , ( ) => {
25+ describe . each ( [
26+ [ true , 'browser' ] ,
27+ [ false , 'server' ] ,
28+ ] ) ( 'themeStore %s' , ( _isBrowser , _title ) => {
29+ const originalWindow = globalThis . window
2430 beforeEach ( ( ) => {
31+ // Remove window to simulate SSR
32+ if ( ! _isBrowser ) {
33+ // @ts -expect-error - Temporarily remove window for SSR test
34+ delete globalThis . window
35+ }
2536 document . documentElement . removeAttribute ( 'data-theme' )
2637 } )
2738
2839 afterEach ( ( ) => {
2940 document . documentElement . removeAttribute ( 'data-theme' )
41+ globalThis . window = originalWindow
3042 } )
31-
32- it ( 'should return themeStore object for browser' , async ( ) => {
33- // const modulePath = require.resolve('../theme-store')
34- // delete require.cache[modulePath]
35- const { createThemeStore } = await import ( '../theme-store' )
36- const themeStore = createThemeStore ( )
37- expect ( themeStore ) . toBeDefined ( )
38- expect ( themeStore . get ) . toEqual ( expect . any ( Function ) )
39- expect ( themeStore . set ) . toEqual ( expect . any ( Function ) )
40- expect ( themeStore . subscribe ) . toEqual ( expect . any ( Function ) )
41- expect ( themeStore . get ( ) ) . toBeNull ( )
42- expect ( themeStore . set ( 'dark' as any ) ) . toBeUndefined ( )
43- // subscribe returns an unsubscribe function, which returns boolean when called
44- const unsubscribe = themeStore . subscribe ( ( ) => { } )
45- expect ( typeof unsubscribe ) . toBe ( 'function' )
46- themeStore . subscribe ( ( ) => { } )
47- expect ( themeStore . set ( 'dark' as any ) ) . toBeUndefined ( )
48- } )
49-
50- it ( 'should call subscriber when theme changes via set' , async ( ) => {
51- // const modulePath = require.resolve('../theme-store')
52- // delete require.cache[modulePath]
53- const { createThemeStore } = await import ( '../theme-store' )
54- const themeStore = createThemeStore ( )
55- const callback = mock ( )
56-
57- themeStore . subscribe ( callback )
58-
59- // First call is from subscribe itself (reads current data-theme)
60- expect ( callback ) . toHaveBeenCalledTimes ( 1 )
61-
62- themeStore . set ( 'light' as any )
63- expect ( callback ) . toHaveBeenCalledTimes ( 2 )
64- expect ( callback ) . toHaveBeenLastCalledWith ( 'light' )
65- expect ( themeStore . get ( ) ) . toBe ( 'light' as any )
66- } )
67-
68- it ( 'should unsubscribe correctly' , async ( ) => {
69- // const modulePath = require.resolve('../theme-store')
70- // delete require.cache[modulePath]
71- const { createThemeStore } = await import ( '../theme-store' )
72- const themeStore = createThemeStore ( )
73- const callback = mock ( )
74-
75- const unsubscribe = themeStore . subscribe ( callback )
76- expect ( callback ) . toHaveBeenCalledTimes ( 1 )
77-
78- // Unsubscribe
79- const result = unsubscribe ( )
80- expect ( result ) . toBe ( true as any )
81-
82- // Should not be called after unsubscribe
83- themeStore . set ( 'dark' as any )
84- expect ( callback ) . toHaveBeenCalledTimes ( 1 )
85- } )
86-
87- it ( 'should read initial theme from data-theme attribute' , async ( ) => {
88- document . documentElement . setAttribute ( 'data-theme' , 'dark' )
89-
90- // const modulePath = require.resolve('../theme-store')
91- // delete require.cache[modulePath]
92- const { createThemeStore } = await import ( '../theme-store' )
93- const themeStore = createThemeStore ( )
94- const callback = mock ( )
95-
96- themeStore . subscribe ( callback )
97-
98- // Should be called with 'dark' from the attribute
99- expect ( callback ) . toHaveBeenCalledWith ( 'dark' )
100- } )
101-
102- it ( 'should update theme when data-theme attribute changes via MutationObserver' , async ( ) => {
103- // const modulePath = require.resolve('../theme-store')
104- // delete require.cache[modulePath]
105- const { createThemeStore } = await import ( '../theme-store' )
106- const themeStore = createThemeStore ( )
107- const callback = mock ( )
108-
109- themeStore . subscribe ( callback )
110- expect ( callback ) . toHaveBeenCalledTimes ( 1 )
111-
112- // Change the attribute - MutationObserver should trigger
113- document . documentElement . setAttribute ( 'data-theme' , 'dark' )
114-
115- // Wait for MutationObserver to fire (it's async)
116- await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
117-
118- expect ( callback ) . toHaveBeenCalledWith ( 'dark' )
119- expect ( themeStore . get ( ) ) . toBe ( 'dark' as any )
120- } )
121-
122- it ( 'should handle multiple subscribers' , async ( ) => {
123- // const modulePath = require.resolve('../theme-store')
124- // delete require.cache[modulePath]
125- const { createThemeStore } = await import ( '../theme-store' )
126- const themeStore = createThemeStore ( )
127- const callback1 = mock ( )
128- const callback2 = mock ( )
129-
130- themeStore . subscribe ( callback1 )
131- themeStore . subscribe ( callback2 )
132-
133- themeStore . set ( 'light' as any )
134-
135- expect ( callback1 ) . toHaveBeenLastCalledWith ( 'light' )
136- expect ( callback2 ) . toHaveBeenLastCalledWith ( 'light' )
137- } )
138-
139- it ( 'should filter mutations by type and target' , async ( ) => {
140- // const modulePath = require.resolve('../theme-store')
141- // delete require.cache[modulePath]
142- const { createThemeStore } = await import ( '../theme-store' )
143- const themeStore = createThemeStore ( )
144- const callback = mock ( )
145-
146- themeStore . subscribe ( callback )
147-
148- // Change data-theme attribute (should trigger)
149- document . documentElement . setAttribute ( 'data-theme' , 'system' )
150-
151- await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
152-
153- expect ( themeStore . get ( ) ) . toBe ( 'system' as any )
154- } )
43+ if ( _isBrowser ) {
44+ it ( 'should return themeStore object for browser' , async ( ) => {
45+ // const modulePath = require.resolve('../theme-store')
46+ // delete require.cache[modulePath]
47+ const { createThemeStore } = await import ( '../theme-store' )
48+ const themeStore = createThemeStore ( )
49+ expect ( themeStore ) . toBeDefined ( )
50+ expect ( themeStore . get ) . toEqual ( expect . any ( Function ) )
51+ expect ( themeStore . set ) . toEqual ( expect . any ( Function ) )
52+ expect ( themeStore . subscribe ) . toEqual ( expect . any ( Function ) )
53+ expect ( themeStore . get ( ) ) . toBeNull ( )
54+ expect ( themeStore . set ( 'dark' as any ) ) . toBeUndefined ( )
55+ // subscribe returns an unsubscribe function, which returns boolean when called
56+ const unsubscribe = themeStore . subscribe ( ( ) => { } )
57+ expect ( typeof unsubscribe ) . toBe ( 'function' )
58+ themeStore . subscribe ( ( ) => { } )
59+ expect ( themeStore . set ( 'dark' as any ) ) . toBeUndefined ( )
60+ } )
61+
62+ it ( 'should call subscriber when theme changes via set' , async ( ) => {
63+ // const modulePath = require.resolve('../theme-store')
64+ // delete require.cache[modulePath]
65+ const { createThemeStore } = await import ( '../theme-store' )
66+ const themeStore = createThemeStore ( )
67+ const callback = mock ( )
68+
69+ themeStore . subscribe ( callback )
70+
71+ // First call is from subscribe itself (reads current data-theme)
72+ expect ( callback ) . toHaveBeenCalledTimes ( 1 )
73+
74+ themeStore . set ( 'light' as any )
75+ expect ( callback ) . toHaveBeenCalledTimes ( 2 )
76+ expect ( callback ) . toHaveBeenLastCalledWith ( 'light' )
77+ expect ( themeStore . get ( ) ) . toBe ( 'light' as any )
78+ } )
79+
80+ it ( 'should unsubscribe correctly' , async ( ) => {
81+ // const modulePath = require.resolve('../theme-store')
82+ // delete require.cache[modulePath]
83+ const { createThemeStore } = await import ( '../theme-store' )
84+ const themeStore = createThemeStore ( )
85+ const callback = mock ( )
86+
87+ const unsubscribe = themeStore . subscribe ( callback )
88+ expect ( callback ) . toHaveBeenCalledTimes ( 1 )
89+
90+ // Unsubscribe
91+ const result = unsubscribe ( )
92+ expect ( result ) . toBe ( true as any )
93+
94+ // Should not be called after unsubscribe
95+ themeStore . set ( 'dark' as any )
96+ expect ( callback ) . toHaveBeenCalledTimes ( 1 )
97+ } )
98+
99+ it ( 'should read initial theme from data-theme attribute' , async ( ) => {
100+ document . documentElement . setAttribute ( 'data-theme' , 'dark' )
101+
102+ // const modulePath = require.resolve('../theme-store')
103+ // delete require.cache[modulePath]
104+ const { createThemeStore } = await import ( '../theme-store' )
105+ const themeStore = createThemeStore ( )
106+ const callback = mock ( )
107+
108+ themeStore . subscribe ( callback )
109+
110+ // Should be called with 'dark' from the attribute
111+ expect ( callback ) . toHaveBeenCalledWith ( 'dark' )
112+ } )
113+
114+ it ( 'should update theme when data-theme attribute changes via MutationObserver' , async ( ) => {
115+ // const modulePath = require.resolve('../theme-store')
116+ // delete require.cache[modulePath]
117+ const { createThemeStore } = await import ( '../theme-store' )
118+ const themeStore = createThemeStore ( )
119+ const callback = mock ( )
120+
121+ themeStore . subscribe ( callback )
122+ expect ( callback ) . toHaveBeenCalledTimes ( 1 )
123+
124+ // Change the attribute - MutationObserver should trigger
125+ document . documentElement . setAttribute ( 'data-theme' , 'dark' )
126+
127+ // Wait for MutationObserver to fire (it's async)
128+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
129+
130+ expect ( callback ) . toHaveBeenCalledWith ( 'dark' )
131+ expect ( themeStore . get ( ) ) . toBe ( 'dark' as any )
132+ } )
133+
134+ it ( 'should handle multiple subscribers' , async ( ) => {
135+ // const modulePath = require.resolve('../theme-store')
136+ // delete require.cache[modulePath]
137+ const { createThemeStore } = await import ( '../theme-store' )
138+ const themeStore = createThemeStore ( )
139+ const callback1 = mock ( )
140+ const callback2 = mock ( )
141+
142+ themeStore . subscribe ( callback1 )
143+ themeStore . subscribe ( callback2 )
144+
145+ themeStore . set ( 'light' as any )
146+
147+ expect ( callback1 ) . toHaveBeenLastCalledWith ( 'light' )
148+ expect ( callback2 ) . toHaveBeenLastCalledWith ( 'light' )
149+ } )
150+
151+ it ( 'should filter mutations by type and target' , async ( ) => {
152+ // const modulePath = require.resolve('../theme-store')
153+ // delete require.cache[modulePath]
154+ const { createThemeStore } = await import ( '../theme-store' )
155+ const themeStore = createThemeStore ( )
156+ const callback = mock ( )
157+
158+ themeStore . subscribe ( callback )
159+
160+ // Change data-theme attribute (should trigger)
161+ document . documentElement . setAttribute ( 'data-theme' , 'system' )
162+
163+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
164+
165+ expect ( themeStore . get ( ) ) . toBe ( 'system' as any )
166+ } )
167+ } else {
168+ it ( 'should return themeStore object for server' , async ( ) => {
169+ const themeStore = createThemeStore ( )
170+
171+ // Test SSR store behavior
172+ expect ( themeStore ) . toBeDefined ( )
173+ expect ( themeStore . get ( ) ) . toBeNull ( )
174+ expect ( themeStore . set ( 'dark' as any ) ) . toBeUndefined ( )
175+
176+ const unsubscribe = themeStore . subscribe ( ( ) => { } )
177+ expect ( typeof unsubscribe ) . toBe ( 'function' )
178+
179+ // The unsubscribe should return a no-op function
180+ const innerUnsubscribe = unsubscribe ( )
181+ expect ( innerUnsubscribe ) . toBeUndefined ( )
182+ } )
183+ }
155184} )
0 commit comments