@@ -116,7 +116,7 @@ describe('landing page warm-up', () => {
116116} ) ;
117117
118118describe ( 'landing warm-up during a worker upgrade' , ( ) => {
119- function setup ( effectiveType ?: string ) {
119+ function setup ( effectiveType ?: string , controlled = true ) {
120120 const src = readFileSync ( resolve ( __dirname , '../../public/landing-prefetch.js' ) , 'utf8' ) ;
121121 const win = Object . assign ( new EventTarget ( ) , {
122122 requestIdleCallback : ( callback : ( ) => void ) => callback ( ) ,
@@ -128,7 +128,10 @@ describe('landing warm-up during a worker upgrade', () => {
128128 waiting : null as ( EventTarget & { state : string ; scriptURL ?: string } ) | null ,
129129 installing : null as ( EventTarget & { state : string } ) | null ,
130130 } ) ;
131- const workers = Object . assign ( new EventTarget ( ) , { ready : Promise . resolve ( registration ) } ) ;
131+ const workers = Object . assign ( new EventTarget ( ) , {
132+ ready : Promise . resolve ( registration ) ,
133+ controller : controlled ? ( { } as object | null ) : null ,
134+ } ) ;
132135 const fetchMock = vi . fn ( ( _url : string , _init ?: RequestInit ) => Promise . resolve ( okResponse ( ) ) ) ;
133136 new Function ( 'window' , 'document' , 'navigator' , 'fetch' , 'AbortController' , src ) (
134137 win ,
@@ -140,6 +143,39 @@ describe('landing warm-up during a worker upgrade', () => {
140143 return { win, doc, cta, registration, workers, fetchMock, hook : win . __landingPrefetch ! } ;
141144 }
142145
146+ it ( 'waits for actual page control after ready before fetching a cacheable resource' , async ( ) => {
147+ const { workers, hook, fetchMock } = setup ( undefined , false ) ;
148+ const cached = new Set < string > ( ) ;
149+ fetchMock . mockImplementation ( ( url ) => {
150+ if ( workers . controller ) cached . add ( url ) ;
151+ return Promise . resolve ( okResponse ( ) ) ;
152+ } ) ;
153+ const pending = hook . warm ( '/sdkjs/common/libfont/engine/fonts.js' ) ;
154+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
155+ expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
156+ expect ( cached . size ) . toBe ( 0 ) ;
157+
158+ workers . controller = { } ;
159+ workers . dispatchEvent ( new Event ( 'controllerchange' ) ) ;
160+ await pending ;
161+ expect ( cached . has ( '/sdkjs/common/libfont/engine/fonts.js' ) ) . toBe ( true ) ;
162+ } ) ;
163+
164+ it ( 'settles a control wait on departure and leaves the URL retryable after returning' , async ( ) => {
165+ const { workers, win, hook, fetchMock } = setup ( undefined , false ) ;
166+ const pending = hook . warm ( '/awaiting-control.js' ) ;
167+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
168+ win . dispatchEvent ( new Event ( 'beforeunload' ) ) ;
169+ await pending ;
170+ workers . controller = { } ;
171+ workers . dispatchEvent ( new Event ( 'controllerchange' ) ) ;
172+ expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
173+
174+ win . dispatchEvent ( new Event ( 'pointerdown' ) ) ;
175+ await hook . warm ( '/awaiting-control.js' ) ;
176+ expect ( fetchMock . mock . calls . filter ( ( [ url ] ) => url === '/awaiting-control.js' ) ) . toHaveLength ( 1 ) ;
177+ } ) ;
178+
143179 it ( 'holds a waiting upgrade without poisoning URLs, then warms after controllerchange' , async ( ) => {
144180 const { registration, workers, win, hook, fetchMock } = setup ( ) ;
145181 registration . waiting = Object . assign ( new EventTarget ( ) , { state : 'installed' } ) ;
0 commit comments