1414 * limitations under the License.
1515 */
1616
17- import { splitProgress } from './progress' ;
17+ import { Progress , splitProgress } from './progress' ;
1818import { SnapshotServer } from './snapshotServer' ;
1919import { TraceModel } from './traceModel' ;
2020import { FetchTraceModelBackend , traceFileURL , ZipTraceModelBackend } from './traceModelBackends' ;
@@ -41,6 +41,13 @@ type ServiceWorkerGlobalScope = {
4141 skipWaiting ( ) : Promise < void > ;
4242} ;
4343
44+ type FetchEvent = {
45+ request : Request ;
46+ clientId : string | null ;
47+ resultingClientId : string | null ;
48+ respondWith ( response : Promise < Response > ) : void ;
49+ } ;
50+
4451declare const self : ServiceWorkerGlobalScope ;
4552
4653self . addEventListener ( 'install' , function ( event : any ) {
@@ -57,145 +64,178 @@ type LoadedTrace = {
5764} ;
5865
5966const scopePath = new URL ( self . registration . scope ) . pathname ;
60- const loadedTraces = new Map < string , LoadedTrace > ( ) ;
67+ const loadedTraces = new Map < string , Promise < LoadedTrace > > ( ) ;
6168const clientIdToTraceUrls = new Map < string , string > ( ) ;
6269const isDeployedAsHttps = self . registration . scope . startsWith ( 'https://' ) ;
6370
64- async function loadTrace ( traceUrl : string , traceFileName : string | null , client : Client ) : Promise < TraceModel > {
65- const clientId = client . id ;
71+ function simulateRestart ( ) {
72+ loadedTraces . clear ( ) ;
73+ clientIdToTraceUrls . clear ( ) ;
74+ }
75+
76+ async function loadTraceOrError ( clientId : string , url : URL , isContextRequest : boolean , progress : Progress ) : Promise < { loadedTrace ?: LoadedTrace , errorResponse ?: Response } > {
77+ try {
78+ const loadedTrace = await loadTrace ( clientId , url , isContextRequest , progress ) ;
79+ return { loadedTrace } ;
80+ } catch ( error ) {
81+ return {
82+ errorResponse : new Response ( JSON . stringify ( { error : error ?. message } ) , {
83+ status : 500 ,
84+ headers : { 'Content-Type' : 'application/json' }
85+ } )
86+ } ;
87+ }
88+ }
89+
90+ function loadTrace ( clientId : string , url : URL , isContextRequest : boolean , progress : Progress ) : Promise < LoadedTrace > {
91+ const traceUrl = url . searchParams . get ( 'trace' ) ! ;
92+ if ( ! traceUrl )
93+ throw new Error ( 'trace parameter is missing' ) ;
94+
6695 clientIdToTraceUrls . set ( clientId , traceUrl ) ;
96+ const omitCache = isContextRequest && isLiveTrace ( traceUrl ) ;
97+ const loadedTrace = omitCache ? undefined : loadedTraces . get ( traceUrl ) ;
98+ if ( loadedTrace )
99+ return loadedTrace ;
100+ const promise = innerLoadTrace ( traceUrl , progress ) ;
101+ loadedTraces . set ( traceUrl , promise ) ;
102+ return promise ;
103+ }
104+
105+ async function innerLoadTrace ( traceUrl : string , progress : Progress ) : Promise < LoadedTrace > {
67106 await gc ( ) ;
68107
69108 const traceModel = new TraceModel ( ) ;
70109 try {
71110 // Allow 10% to hop from sw to page.
72- const [ fetchProgress , unzipProgress ] = splitProgress ( ( done : number , total : number ) => {
73- client . postMessage ( { method : 'progress' , params : { done, total } } ) ;
74- } , [ 0.5 , 0.4 , 0.1 ] ) ;
75- const backend = traceUrl . endsWith ( 'json' ) ? new FetchTraceModelBackend ( traceUrl ) : new ZipTraceModelBackend ( traceUrl , fetchProgress ) ;
111+ const [ fetchProgress , unzipProgress ] = splitProgress ( progress , [ 0.5 , 0.4 , 0.1 ] ) ;
112+ const backend = isLiveTrace ( traceUrl ) ? new FetchTraceModelBackend ( traceUrl ) : new ZipTraceModelBackend ( traceUrl , fetchProgress ) ;
76113 await traceModel . load ( backend , unzipProgress ) ;
77114 } catch ( error : any ) {
78115 // eslint-disable-next-line no-console
79116 console . error ( error ) ;
80117 if ( error ?. message ?. includes ( 'Cannot find .trace file' ) && await traceModel . hasEntry ( 'index.html' ) )
81118 throw new Error ( 'Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.' ) ;
82119 if ( error instanceof TraceVersionError )
83- throw new Error ( `Could not load trace from ${ traceFileName || traceUrl } . ${ error . message } ` ) ;
84- if ( traceFileName )
85- throw new Error ( `Could not load trace from ${ traceFileName } . Make sure to upload a valid Playwright trace.` ) ;
120+ throw new Error ( `Could not load trace from ${ traceUrl } . ${ error . message } ` ) ;
86121 throw new Error ( `Could not load trace from ${ traceUrl } . Make sure a valid Playwright Trace is accessible over this url.` ) ;
87122 }
88123 const snapshotServer = new SnapshotServer ( traceModel . storage ( ) , sha1 => traceModel . resourceForSha1 ( sha1 ) ) ;
89- loadedTraces . set ( traceUrl , { traceModel, snapshotServer } ) ;
90- return traceModel ;
124+ return { traceModel, snapshotServer } ;
91125}
92126
93- // @ts -ignore
94127async function doFetch ( event : FetchEvent ) : Promise < Response > {
128+ const request = event . request ;
129+
95130 // In order to make Accessibility Insights for Web work.
96- if ( event . request . url . startsWith ( 'chrome-extension://' ) )
97- return fetch ( event . request ) ;
131+ if ( request . url . startsWith ( 'chrome-extension://' ) )
132+ return fetch ( request ) ;
98133
99- if ( event . request . headers . get ( 'x-pw-serviceworker' ) === 'forward' ) {
134+ if ( request . headers . get ( 'x-pw-serviceworker' ) === 'forward' ) {
100135 const request = new Request ( event . request ) ;
101136 request . headers . delete ( 'x-pw-serviceworker' ) ;
102137 return fetch ( request ) ;
103138 }
104139
105- const request = event . request ;
106- const client = await self . clients . get ( event . clientId ) as Client | undefined ;
107-
108- // When trace viewer is deployed over https, we will force upgrade
109- // insecure http subresources to https. Otherwise, these will fail
110- // to load inside our https snapshots.
111- // In this case, we also match http resources from the archive by
112- // the https urls.
113140 const url = new URL ( request . url ) ;
114-
115141 let relativePath : string | undefined ;
116142 if ( request . url . startsWith ( self . registration . scope ) )
117143 relativePath = url . pathname . substring ( scopePath . length - 1 ) ;
118144
145+ if ( relativePath === '/restartServiceWorker' ) {
146+ simulateRestart ( ) ;
147+ return new Response ( null , { status : 200 } ) ;
148+ }
149+
119150 if ( relativePath === '/ping' )
120151 return new Response ( null , { status : 200 } ) ;
121152
122- if ( relativePath === '/contexts' ) {
123- const traceUrl = url . searchParams . get ( 'trace' ) ;
124- if ( ! client || ! traceUrl ) {
125- return new Response ( 'Something went wrong, trace is requested as a part of the navigation' , {
126- status : 500 ,
127- headers : { 'Content-Type' : 'application/json' }
128- } ) ;
153+ const isNavigation = ! ! event . resultingClientId ;
154+ const client = event . clientId ? await self . clients . get ( event . clientId ) : undefined ;
155+
156+ if ( isNavigation && ! relativePath ?. startsWith ( '/sha1/' ) ) {
157+ // Navigation request. Download is a /sha1/ navigation, ignore them here.
158+
159+ // Snapshot iframe navigation request.
160+ if ( relativePath ?. startsWith ( '/snapshot/' ) ) {
161+ // It is Ok to pass noop progress as the trace is likely already loaded.
162+ const { errorResponse, loadedTrace } = await loadTraceOrError ( event . resultingClientId ! , url , false , noopProgress ) ;
163+ if ( errorResponse )
164+ return errorResponse ;
165+ const pageOrFrameId = relativePath . substring ( '/snapshot/' . length ) ;
166+ const response = loadedTrace ! . snapshotServer . serveSnapshot ( pageOrFrameId , url . searchParams , url . href ) ;
167+ if ( isDeployedAsHttps )
168+ response . headers . set ( 'Content-Security-Policy' , 'upgrade-insecure-requests' ) ;
169+ return response ;
129170 }
130171
131- try {
132- const traceModel = await loadTrace ( traceUrl , url . searchParams . get ( 'traceFileName' ) , client ) ;
133- return new Response ( JSON . stringify ( traceModel . contextEntries ) , {
134- status : 200 ,
135- headers : { 'Content-Type' : 'application/json' }
136- } ) ;
137- } catch ( error : any ) {
138- return new Response ( JSON . stringify ( { error : error ?. message } ) , {
139- status : 500 ,
140- headers : { 'Content-Type' : 'application/json' }
141- } ) ;
142- }
172+ // Static content navigation request for trace viewer or popout.
173+ return fetch ( event . request ) ;
143174 }
144175
145- if ( relativePath ?. startsWith ( '/snapshotInfo/' ) ) {
146- const { snapshotServer } = loadedTrace ( url ) ;
147- if ( ! snapshotServer )
148- return new Response ( null , { status : 404 } ) ;
149- const pageOrFrameId = relativePath . substring ( '/snapshotInfo/' . length ) ;
150- return snapshotServer . serveSnapshotInfo ( pageOrFrameId , url . searchParams ) ;
151- }
176+ if ( ! relativePath ) {
177+ // Out-of-scope sub-resource request => iframe snapshot sub-resources.
178+ if ( ! client )
179+ return new Response ( 'Sub-resource without a client' , { status : 500 } ) ;
152180
153- if ( relativePath ?. startsWith ( '/snapshot/' ) ) {
154- const { snapshotServer } = loadedTrace ( url ) ;
181+ const { snapshotServer } = await loadTrace ( client . id , new URL ( client . url ) , false , clientProgress ( client ) ) ;
155182 if ( ! snapshotServer )
156183 return new Response ( null , { status : 404 } ) ;
157- const pageOrFrameId = relativePath . substring ( '/snapshot/' . length ) ;
158- const response = snapshotServer . serveSnapshot ( pageOrFrameId , url . searchParams , url . href ) ;
159- if ( isDeployedAsHttps )
160- response . headers . set ( 'Content-Security-Policy' , 'upgrade-insecure-requests' ) ;
161- return response ;
162- }
163184
164- if ( relativePath ?. startsWith ( '/closest-screenshot/' ) ) {
165- const { snapshotServer } = loadedTrace ( url ) ;
166- if ( ! snapshotServer )
167- return new Response ( null , { status : 404 } ) ;
168- const pageOrFrameId = relativePath . substring ( '/closest-screenshot/' . length ) ;
169- return snapshotServer . serveClosestScreenshot ( pageOrFrameId , url . searchParams ) ;
185+ // When trace viewer is deployed over https, we will force upgrade
186+ // insecure http sub-resources to https. Otherwise, these will fail
187+ // to load inside our https snapshots.
188+ // In this case, we also match http resources from the archive by
189+ // the https urls.
190+ const lookupUrls = [ request . url ] ;
191+ if ( isDeployedAsHttps && request . url . startsWith ( 'https://' ) )
192+ lookupUrls . push ( request . url . replace ( / ^ h t t p s / , 'http' ) ) ;
193+ return snapshotServer . serveResource ( lookupUrls , request . method , client . url ) ;
170194 }
171195
172- if ( relativePath ?. startsWith ( '/sha1/' ) ) {
173- const { traceModel } = loadedTrace ( url ) ;
174- const blob = await traceModel ?. resourceForSha1 ( relativePath . slice ( '/sha1/' . length ) ) ;
175- if ( blob )
176- return new Response ( blob , { status : 200 , headers : downloadHeaders ( url . searchParams ) } ) ;
177- return new Response ( null , { status : 404 } ) ;
196+ // These commands all require a loaded trace.
197+ if ( relativePath === '/contexts' || relativePath . startsWith ( '/snapshotInfo/' ) || relativePath . startsWith ( '/closest-screenshot/' ) || relativePath . startsWith ( '/sha1/' ) ) {
198+ if ( ! client )
199+ return new Response ( 'Sub-resource without a client' , { status : 500 } ) ;
200+
201+ const isContextRequest = relativePath === '/contexts' ;
202+ const { errorResponse, loadedTrace } = await loadTraceOrError ( client . id , url , isContextRequest , clientProgress ( client ) ) ;
203+ if ( errorResponse )
204+ return errorResponse ;
205+
206+ if ( relativePath === '/contexts' ) {
207+ return new Response ( JSON . stringify ( loadedTrace ! . traceModel . contextEntries ) , {
208+ status : 200 ,
209+ headers : { 'Content-Type' : 'application/json' }
210+ } ) ;
211+ }
212+
213+ if ( relativePath . startsWith ( '/snapshotInfo/' ) ) {
214+ const pageOrFrameId = relativePath . substring ( '/snapshotInfo/' . length ) ;
215+ return loadedTrace ! . snapshotServer . serveSnapshotInfo ( pageOrFrameId , url . searchParams ) ;
216+ }
217+
218+ if ( relativePath . startsWith ( '/closest-screenshot/' ) ) {
219+ const pageOrFrameId = relativePath . substring ( '/closest-screenshot/' . length ) ;
220+ return loadedTrace ! . snapshotServer . serveClosestScreenshot ( pageOrFrameId , url . searchParams ) ;
221+ }
222+
223+ if ( relativePath . startsWith ( '/sha1/' ) ) {
224+ const blob = await loadedTrace ! . traceModel . resourceForSha1 ( relativePath . slice ( '/sha1/' . length ) ) ;
225+ if ( blob )
226+ return new Response ( blob , { status : 200 , headers : downloadHeaders ( url . searchParams ) } ) ;
227+ return new Response ( null , { status : 404 } ) ;
228+ }
178229 }
179230
231+ // Pass through to the server for file requests.
180232 if ( relativePath ?. startsWith ( '/file/' ) ) {
181233 const path = url . searchParams . get ( 'path' ) ! ;
182234 return await fetch ( traceFileURL ( path ) ) ;
183235 }
184236
185- // Fallback for static assets.
186- if ( relativePath )
187- return fetch ( event . request ) ;
188-
189- const snapshotUrl = client ! . url ;
190- const traceUrl = new URL ( snapshotUrl ) . searchParams . get ( 'trace' ) ! ;
191- const { snapshotServer } = loadedTraces . get ( traceUrl ) || { } ;
192- if ( ! snapshotServer )
193- return new Response ( null , { status : 404 } ) ;
194-
195- const lookupUrls = [ request . url ] ;
196- if ( isDeployedAsHttps && request . url . startsWith ( 'https://' ) )
197- lookupUrls . push ( request . url . replace ( / ^ h t t p s / , 'http' ) ) ;
198- return snapshotServer . serveResource ( lookupUrls , request . method , snapshotUrl ) ;
237+ // Static content for sub-resource.
238+ return fetch ( event . request ) ;
199239}
200240
201241function downloadHeaders ( searchParams : URLSearchParams ) : Headers | undefined {
@@ -210,19 +250,11 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined {
210250 return headers ;
211251}
212252
213- const emptyLoadedTrace = { traceModel : undefined , snapshotServer : undefined } ;
214-
215- function loadedTrace ( url : URL ) : LoadedTrace | { traceModel : undefined , snapshotServer : undefined } {
216- const traceUrl = url . searchParams . get ( 'trace' ) ;
217- return traceUrl ? loadedTraces . get ( traceUrl ) ?? emptyLoadedTrace : emptyLoadedTrace ;
218- }
219-
220253async function gc ( ) {
221254 const clients = await self . clients . matchAll ( ) ;
222255 const usedTraces = new Set < string > ( ) ;
223256
224257 for ( const [ clientId , traceUrl ] of clientIdToTraceUrls ) {
225- // @ts -ignore
226258 if ( ! clients . find ( c => c . id === clientId ) ) {
227259 clientIdToTraceUrls . delete ( clientId ) ;
228260 continue ;
@@ -236,7 +268,18 @@ async function gc() {
236268 }
237269}
238270
239- // @ts -ignore
271+ function clientProgress ( client : Client ) : Progress {
272+ return ( done : number , total : number ) => {
273+ client . postMessage ( { method : 'progress' , params : { done, total } } ) ;
274+ } ;
275+ }
276+
277+ function noopProgress ( done : number , total : number ) : undefined { }
278+
279+ function isLiveTrace ( traceUrl : string ) : boolean {
280+ return traceUrl . endsWith ( '.json' ) ;
281+ }
282+
240283self . addEventListener ( 'fetch' , function ( event : FetchEvent ) {
241284 event . respondWith ( doFetch ( event ) ) ;
242285} ) ;
0 commit comments