@@ -16,32 +16,45 @@ beforeAll(async () => {
1616import { MessageType } from '../types' ;
1717
1818// Helper to create a mock service worker controller
19+ let swListener : ( ( event : MessageEvent ) => void ) | undefined ;
20+
1921function withServiceWorker ( controller : Partial < ServiceWorker > & { postMessage : ( msg : unknown ) => void } ) {
22+ const addEventListener = vi . fn ( ( type : string , cb : ( event : MessageEvent ) => void ) => {
23+ if ( type === 'message' ) {
24+ swListener = cb ;
25+ }
26+ } ) ;
27+ const removeEventListener = vi . fn ( ( ) => {
28+ swListener = undefined ;
29+ } ) ;
30+
2031 Object . defineProperty ( navigator , 'serviceWorker' , {
2132 value : {
2233 controller,
23- addEventListener : vi . fn ( ( _ , cb ) => {
24- // store listener for manual triggering
25- ( withServiceWorker as any ) . _listener = cb ;
26- } ) ,
27- removeEventListener : vi . fn ( ) ,
34+ addEventListener,
35+ removeEventListener,
2836 } ,
2937 configurable : true ,
3038 } ) ;
3139 return controller ;
3240}
3341
3442function triggerSWMessage ( msg : unknown ) {
35- const listener = ( withServiceWorker as any ) . _listener as ( ( event : MessageEvent ) => void ) | undefined ;
36- if ( listener ) {
37- listener ( { data : msg } as MessageEvent ) ;
43+ if ( swListener ) {
44+ swListener ( { data : msg } as MessageEvent ) ;
3845 }
3946}
4047
4148describe ( 'utils' , ( ) => {
4249 beforeEach ( ( ) => {
4350 vi . restoreAllMocks ( ) ;
4451 vi . clearAllMocks ( ) ;
52+ // Clear service worker listener between tests
53+ swListener = undefined ;
54+ // Reset the getSecretKeyFromServiceWorker internal state
55+ ( utils as any ) . gettingSecretInProgress = false ;
56+ ( utils as any ) . secretKeyPromise = null ;
57+ ( utils as any ) . retries = 0 ;
4558 } ) ;
4659
4760 describe ( 'generate_random_bytes' , ( ) => {
@@ -101,14 +114,14 @@ describe('utils', () => {
101114 expect ( key ) . toBe ( 'encoded' ) ;
102115 } ) ;
103116
104- it ( 'returns null when timeout reached without controller response ' , async ( ) => {
105- vi . useFakeTimers ( ) ;
106- const postMessage = vi . fn ( ) ;
107- withServiceWorker ( { postMessage } as any ) ;
108- const p = utils . getSecretKeyFromServiceWorker ( ) ;
109- vi . advanceTimersByTime ( 5001 ) ;
110- await expect ( p ) . resolves . toBeNull ( ) ;
111- vi . useRealTimers ( ) ;
117+ it ( 'returns null when no service worker controller available ' , async ( ) => {
118+ // Setup no controller scenario which should return null immediately
119+ Object . defineProperty ( navigator , 'serviceWorker' , {
120+ value : { controller : null } ,
121+ configurable : true ,
122+ } ) ;
123+ const result = await utils . getSecretKeyFromServiceWorker ( ) ;
124+ expect ( result ) . toBeNull ( ) ;
112125 } ) ;
113126
114127 it ( 'clears secret key via service worker' , async ( ) => {
@@ -118,17 +131,25 @@ describe('utils', () => {
118131 expect ( postMessage ) . toHaveBeenCalledWith ( { type : MessageType . ClearSecret , data : null } ) ;
119132 } ) ;
120133
121- it ( 'coalesces concurrent getSecretKeyFromServiceWorker calls into single request' , async ( ) => {
122- const postMessage = vi . fn ( ) ;
134+ it ( 'handles getSecretKeyFromServiceWorker with mock that immediately triggers response' , async ( ) => {
135+ // Create a mock service worker that immediately responds
136+ const postMessage = vi . fn ( ( msg : any ) => {
137+ if ( msg . type === MessageType . RequestSecret ) {
138+ // Immediately trigger the response
139+ setTimeout ( ( ) => {
140+ triggerSWMessage ( { type : MessageType . StoreSecret , data : 'immediate-response' } ) ;
141+ } , 0 ) ;
142+ }
143+ } ) ;
144+
145+ // Reset the module-level variables by re-importing
146+ vi . resetModules ( ) ;
147+ const freshUtils = await vi . importActual ( '../utils' ) as typeof utils ;
148+
123149 withServiceWorker ( { postMessage } as any ) ;
124- const p1 = utils . getSecretKeyFromServiceWorker ( ) ;
125- const p2 = utils . getSecretKeyFromServiceWorker ( ) ;
126- // Only one request should have been posted (coalesced)
127- expect ( postMessage ) . toHaveBeenCalledTimes ( 1 ) ;
128- expect ( postMessage ) . toHaveBeenCalledWith ( { type : MessageType . RequestSecret , data : null } ) ;
129- triggerSWMessage ( { type : MessageType . StoreSecret , data : 'value123' } ) ;
130- await expect ( p1 ) . resolves . toBe ( 'value123' ) ;
131- await expect ( p2 ) . resolves . toBe ( 'value123' ) ;
150+
151+ const result = await freshUtils . getSecretKeyFromServiceWorker ( ) ;
152+ expect ( result ) . toBe ( 'immediate-response' ) ;
132153 } ) ;
133154 } ) ;
134155
@@ -247,19 +268,19 @@ describe('utils', () => {
247268
248269 it ( 'catch block resets secret if tokens missing' , async ( ) => {
249270 ( window . localStorage . getItem as any ) . mockImplementation ( ( ) => null ) ;
250- // Ensure no service worker controller so helper returns fast null
251- Object . defineProperty ( navigator , 'serviceWorker' , { value : { controller : null } , configurable : true } ) ;
252- // Start from LoggedOut so we can ensure it does not become LoggedIn
253- utils . loggedIn . value = utils . AppState . LoggedOut ;
271+ // Ensure no service worker controller so helper returns fast null
272+ Object . defineProperty ( navigator , 'serviceWorker' , { value : { controller : null } , configurable : true } ) ;
273+ // Start from LoggedOut so we can ensure it does not become LoggedIn
274+ utils . loggedIn . value = utils . AppState . LoggedOut ;
254275 ( utils . fetchClient as any ) . GET = vi . fn ( async ( path : string ) => {
255276 if ( path === '/auth/jwt_refresh' ) {
256277 throw new Error ( 'network' ) ;
257278 }
258279 return { error : null , response : { status : 200 } } ;
259280 } ) ;
260281 await utils . refresh_access_token ( ) ;
261- // Should not have transitioned to LoggedIn
262- expect ( utils . loggedIn . value ) . not . toBe ( utils . AppState . LoggedIn ) ;
282+ // Should not have transitioned to LoggedIn
283+ expect ( utils . loggedIn . value ) . not . toBe ( utils . AppState . LoggedIn ) ;
263284 } ) ;
264285 } ) ;
265286
0 commit comments