@@ -28,6 +28,7 @@ import { selectors } from './selectors';
28
28
import * as types from './types' ;
29
29
import { BrowserContext } from './browserContext' ;
30
30
import { Progress , ProgressController } from './progress' ;
31
+ import { EventEmitter } from 'events' ;
31
32
32
33
type ContextData = {
33
34
contextPromise : Promise < dom . FrameExecutionContext > ;
@@ -51,6 +52,13 @@ type ConsoleTagHandler = () => void;
51
52
52
53
export type FunctionWithSource = ( source : { context : BrowserContext , page : Page , frame : Frame } , ...args : any ) => any ;
53
54
55
+ export const kNavigationEvent = Symbol ( 'navigation' ) ;
56
+ type NavigationEvent = {
57
+ documentInfo ?: DocumentInfo , // Undefined for same-document navigations.
58
+ error ?: Error ,
59
+ } ;
60
+ export const kLifecycleEvent = Symbol ( 'lifecycle' ) ;
61
+
54
62
export class FrameManager {
55
63
private _page : Page ;
56
64
private _frames = new Map < string , Frame > ( ) ;
@@ -158,20 +166,23 @@ export class FrameManager {
158
166
else
159
167
frame . _currentDocument = { documentId, request : undefined } ;
160
168
frame . _pendingDocument = undefined ;
161
- for ( const task of frame . _frameTasks )
162
- task . onNewDocument ( frame . _currentDocument ) ;
169
+ const navigationEvent : NavigationEvent = { documentInfo : frame . _currentDocument } ;
170
+ frame . _eventEmitter . emit ( kNavigationEvent , navigationEvent ) ;
163
171
frame . _onClearLifecycle ( ) ;
164
- if ( ! initial )
172
+ if ( ! initial ) {
173
+ this . _page . _logger . info ( ` navigated to "${ url } "` ) ;
165
174
this . _page . emit ( Events . Page . FrameNavigated , frame ) ;
175
+ }
166
176
}
167
177
168
178
frameCommittedSameDocumentNavigation ( frameId : string , url : string ) {
169
179
const frame = this . _frames . get ( frameId ) ;
170
180
if ( ! frame )
171
181
return ;
172
182
frame . _url = url ;
173
- for ( const task of frame . _frameTasks )
174
- task . onSameDocument ( ) ;
183
+ const navigationEvent : NavigationEvent = { } ;
184
+ frame . _eventEmitter . emit ( kNavigationEvent , navigationEvent ) ;
185
+ this . _page . _logger . info ( ` navigated to "${ url } "` ) ;
175
186
this . _page . emit ( Events . Page . FrameNavigated , frame ) ;
176
187
}
177
188
@@ -181,10 +192,9 @@ export class FrameManager {
181
192
return ;
182
193
if ( documentId !== undefined && frame . _pendingDocument . documentId !== documentId )
183
194
return ;
184
- const pending = frame . _pendingDocument ;
195
+ const navigationEvent : NavigationEvent = { documentInfo : frame . _pendingDocument , error : new Error ( errorText ) } ;
185
196
frame . _pendingDocument = undefined ;
186
- for ( const task of frame . _frameTasks )
187
- task . onNewDocument ( pending , new Error ( errorText ) ) ;
197
+ frame . _eventEmitter . emit ( kNavigationEvent , navigationEvent ) ;
188
198
}
189
199
190
200
frameDetached ( frameId : string ) {
@@ -288,12 +298,12 @@ export class FrameManager {
288
298
}
289
299
290
300
export class Frame {
301
+ readonly _eventEmitter : EventEmitter ;
291
302
_id : string ;
292
303
private _firedLifecycleEvents = new Set < types . LifecycleEvent > ( ) ;
293
304
_subtreeLifecycleEvents = new Set < types . LifecycleEvent > ( ) ;
294
305
_currentDocument : DocumentInfo ;
295
306
_pendingDocument ?: DocumentInfo ;
296
- _frameTasks = new Set < FrameTask > ( ) ;
297
307
readonly _page : Page ;
298
308
private _parentFrame : Frame | null ;
299
309
_url = '' ;
@@ -308,6 +318,8 @@ export class Frame {
308
318
private _detachedCallback = ( ) => { } ;
309
319
310
320
constructor ( page : Page , id : string , parentFrame : Frame | null ) {
321
+ this . _eventEmitter = new EventEmitter ( ) ;
322
+ this . _eventEmitter . setMaxListeners ( 0 ) ;
311
323
this . _id = id ;
312
324
this . _page = page ;
313
325
this . _parentFrame = parentFrame ;
@@ -363,8 +375,9 @@ export class Frame {
363
375
for ( const event of events ) {
364
376
// Checking whether we have already notified about this event.
365
377
if ( ! this . _subtreeLifecycleEvents . has ( event ) ) {
366
- for ( const frameTask of this . _frameTasks )
367
- frameTask . onLifecycle ( event ) ;
378
+ this . _eventEmitter . emit ( kLifecycleEvent , event ) ;
379
+ if ( this === mainFrame && this . _url !== 'about:blank' )
380
+ this . _page . _logger . info ( ` "${ event } " event fired` ) ;
368
381
if ( this === mainFrame && event === 'load' )
369
382
this . _page . emit ( Events . Page . Load ) ;
370
383
if ( this === mainFrame && event === 'domcontentloaded' )
@@ -376,7 +389,8 @@ export class Frame {
376
389
377
390
async goto ( url : string , options : types . GotoOptions = { } ) : Promise < network . Response | null > {
378
391
return runNavigationTask ( this , options , this . _apiName ( 'goto' ) , async progress => {
379
- progress . logger . info ( `navigating to "${ url } ", waiting until "${ options . waitUntil || 'load' } "` ) ;
392
+ const waitUntil = verifyLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
393
+ progress . logger . info ( `navigating to "${ url } ", waiting until "${ waitUntil } "` ) ;
380
394
const headers = ( this . _page . _state . extraHTTPHeaders || { } ) ;
381
395
let referer = headers [ 'referer' ] || headers [ 'Referer' ] ;
382
396
if ( options . referer !== undefined ) {
@@ -386,39 +400,57 @@ export class Frame {
386
400
}
387
401
url = helper . completeUserURL ( url ) ;
388
402
389
- const frameTask = new FrameTask ( this , progress ) ;
390
- const sameDocumentPromise = frameTask . waitForSameDocumentNavigation ( ) ;
391
- const navigateResult = await this . _page . _delegate . navigateFrame ( this , url , referer ) . catch ( e => {
392
- // Do not leave sameDocumentPromise unhandled.
393
- sameDocumentPromise . catch ( e => { } ) ;
394
- throw e ;
395
- } ) ;
396
- let request : network . Request | undefined ;
403
+ const sameDocument = helper . waitForEvent ( progress , this . _eventEmitter , kNavigationEvent , ( e : NavigationEvent ) => ! e . documentInfo ) ;
404
+ const navigateResult = await this . _page . _delegate . navigateFrame ( this , url , referer ) ;
405
+
406
+ let event : NavigationEvent ;
397
407
if ( navigateResult . newDocumentId ) {
398
- // Do not leave sameDocumentPromise unhandled.
399
- sameDocumentPromise . catch ( e => { } ) ;
400
- request = await frameTask . waitForSpecificDocument ( navigateResult . newDocumentId ) ;
408
+ sameDocument . dispose ( ) ;
409
+ event = await helper . waitForEvent ( progress , this . _eventEmitter , kNavigationEvent , ( event : NavigationEvent ) => {
410
+ // We are interested either in this specific document, or any other document that
411
+ // did commit and replaced the expected document.
412
+ return event . documentInfo && ( event . documentInfo . documentId === navigateResult . newDocumentId || ! event . error ) ;
413
+ } ) . promise ;
414
+
415
+ if ( event . documentInfo ! . documentId !== navigateResult . newDocumentId ) {
416
+ // This is just a sanity check. In practice, new navigation should
417
+ // cancel the previous one and report "request cancelled"-like error.
418
+ throw new Error ( 'Navigation interrupted by another one' ) ;
419
+ }
420
+ if ( event . error )
421
+ throw event . error ;
401
422
} else {
402
- await sameDocumentPromise ;
423
+ event = await sameDocument . promise ;
403
424
}
404
- await frameTask . waitForLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
405
- frameTask . done ( ) ;
425
+
426
+ if ( ! this . _subtreeLifecycleEvents . has ( waitUntil ) )
427
+ await helper . waitForEvent ( progress , this . _eventEmitter , kLifecycleEvent , ( e : types . LifecycleEvent ) => e === waitUntil ) . promise ;
428
+
429
+ const request = event . documentInfo ? event . documentInfo . request : undefined ;
406
430
return request ? request . _finalRequest ( ) . response ( ) : null ;
407
431
} ) ;
408
432
}
409
433
410
434
async waitForNavigation ( options : types . WaitForNavigationOptions = { } ) : Promise < network . Response | null > {
411
435
return runNavigationTask ( this , options , this . _apiName ( 'waitForNavigation' ) , async progress => {
412
436
const toUrl = typeof options . url === 'string' ? ` to "${ options . url } "` : '' ;
413
- progress . logger . info ( `waiting for navigation${ toUrl } until "${ options . waitUntil || 'load' } "` ) ;
414
- const frameTask = new FrameTask ( this , progress ) ;
415
- let request : network . Request | undefined ;
416
- await Promise . race ( [
417
- frameTask . waitForNewDocument ( options . url ) . then ( r => request = r ) ,
418
- frameTask . waitForSameDocumentNavigation ( options . url ) ,
419
- ] ) ;
420
- await frameTask . waitForLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
421
- frameTask . done ( ) ;
437
+ const waitUntil = verifyLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
438
+ progress . logger . info ( `waiting for navigation${ toUrl } until "${ waitUntil } "` ) ;
439
+
440
+ const navigationEvent : NavigationEvent = await helper . waitForEvent ( progress , this . _eventEmitter , kNavigationEvent , ( event : NavigationEvent ) => {
441
+ // Any failed navigation results in a rejection.
442
+ if ( event . error )
443
+ return true ;
444
+ progress . logger . info ( ` navigated to "${ this . _url } "` ) ;
445
+ return helper . urlMatches ( this . _url , options . url ) ;
446
+ } ) . promise ;
447
+ if ( navigationEvent . error )
448
+ throw navigationEvent . error ;
449
+
450
+ if ( ! this . _subtreeLifecycleEvents . has ( waitUntil ) )
451
+ await helper . waitForEvent ( progress , this . _eventEmitter , kLifecycleEvent , ( e : types . LifecycleEvent ) => e === waitUntil ) . promise ;
452
+
453
+ const request = navigationEvent . documentInfo ? navigationEvent . documentInfo . request : undefined ;
422
454
return request ? request . _finalRequest ( ) . response ( ) : null ;
423
455
} ) ;
424
456
}
@@ -428,9 +460,9 @@ export class Frame {
428
460
}
429
461
430
462
async _waitForLoadState ( progress : Progress , state : types . LifecycleEvent ) : Promise < void > {
431
- const frameTask = new FrameTask ( this , progress ) ;
432
- await frameTask . waitForLifecycle ( state ) ;
433
- frameTask . done ( ) ;
463
+ const waitUntil = verifyLifecycle ( state ) ;
464
+ if ( ! this . _subtreeLifecycleEvents . has ( waitUntil ) )
465
+ await helper . waitForEvent ( progress , this . _eventEmitter , kLifecycleEvent , ( e : types . LifecycleEvent ) => e === waitUntil ) . promise ;
434
466
}
435
467
436
468
async frameElement ( ) : Promise < dom . ElementHandle > {
@@ -1035,15 +1067,18 @@ class SignalBarrier {
1035
1067
1036
1068
async addFrameNavigation ( frame : Frame ) {
1037
1069
this . retain ( ) ;
1038
- const frameTask = new FrameTask ( frame , this . _progress ) ;
1070
+ const waiter = helper . waitForEvent ( null , frame . _eventEmitter , kNavigationEvent , ( e : NavigationEvent ) => {
1071
+ if ( ! e . error && this . _progress )
1072
+ this . _progress . logger . info ( ` navigated to "${ frame . _url } "` ) ;
1073
+ return true ;
1074
+ } ) ;
1039
1075
await Promise . race ( [
1040
1076
frame . _page . _disconnectedPromise ,
1041
1077
frame . _page . _crashedPromise ,
1042
1078
frame . _detachedPromise ,
1043
- frameTask . waitForNewDocument ( ) ,
1044
- frameTask . waitForSameDocumentNavigation ( ) ,
1079
+ waiter . promise ,
1045
1080
] ) . catch ( e => { } ) ;
1046
- frameTask . done ( ) ;
1081
+ waiter . dispose ( ) ;
1047
1082
this . release ( ) ;
1048
1083
}
1049
1084
@@ -1058,96 +1093,6 @@ class SignalBarrier {
1058
1093
}
1059
1094
}
1060
1095
1061
- class FrameTask {
1062
- private readonly _frame : Frame ;
1063
- private readonly _progress : Progress | null = null ;
1064
- private _onSameDocument ?: { url ?: types . URLMatch , resolve : ( ) => void } ;
1065
- private _onSpecificDocument ?: { expectedDocumentId : string , resolve : ( request : network . Request | undefined ) => void , reject : ( error : Error ) => void } ;
1066
- private _onNewDocument ?: { url ?: types . URLMatch , resolve : ( request : network . Request | undefined ) => void , reject : ( error : Error ) => void } ;
1067
- private _onLifecycle ?: { waitUntil : types . LifecycleEvent , resolve : ( ) => void } ;
1068
-
1069
- constructor ( frame : Frame , progress : Progress | null ) {
1070
- this . _frame = frame ;
1071
- frame . _frameTasks . add ( this ) ;
1072
- this . _progress = progress ;
1073
- if ( progress )
1074
- progress . cleanupWhenAborted ( ( ) => this . done ( ) ) ;
1075
- }
1076
-
1077
- onSameDocument ( ) {
1078
- if ( this . _progress )
1079
- this . _progress . logger . info ( `navigated to "${ this . _frame . _url } "` ) ;
1080
- if ( this . _onSameDocument && helper . urlMatches ( this . _frame . url ( ) , this . _onSameDocument . url ) )
1081
- this . _onSameDocument . resolve ( ) ;
1082
- }
1083
-
1084
- onNewDocument ( documentInfo : DocumentInfo , error ?: Error ) {
1085
- if ( this . _progress && ! error )
1086
- this . _progress . logger . info ( `navigated to "${ this . _frame . _url } "` ) ;
1087
- if ( this . _onSpecificDocument ) {
1088
- if ( documentInfo . documentId === this . _onSpecificDocument . expectedDocumentId ) {
1089
- if ( error )
1090
- this . _onSpecificDocument . reject ( error ) ;
1091
- else
1092
- this . _onSpecificDocument . resolve ( documentInfo . request ) ;
1093
- } else if ( ! error ) {
1094
- this . _onSpecificDocument . reject ( new Error ( 'Navigation interrupted by another one' ) ) ;
1095
- }
1096
- }
1097
- if ( this . _onNewDocument ) {
1098
- if ( error )
1099
- this . _onNewDocument . reject ( error ) ;
1100
- else if ( helper . urlMatches ( this . _frame . url ( ) , this . _onNewDocument . url ) )
1101
- this . _onNewDocument . resolve ( documentInfo . request ) ;
1102
- }
1103
- }
1104
-
1105
- onLifecycle ( lifecycleEvent : types . LifecycleEvent ) {
1106
- if ( this . _progress && this . _frame . _url !== 'about:blank' )
1107
- this . _progress . logger . info ( `"${ lifecycleEvent } " event fired` ) ;
1108
- if ( this . _onLifecycle && this . _onLifecycle . waitUntil === lifecycleEvent )
1109
- this . _onLifecycle . resolve ( ) ;
1110
- }
1111
-
1112
- waitForSameDocumentNavigation ( url ?: types . URLMatch ) : Promise < void > {
1113
- return new Promise ( resolve => {
1114
- assert ( ! this . _onSameDocument ) ;
1115
- this . _onSameDocument = { url, resolve } ;
1116
- } ) ;
1117
- }
1118
-
1119
- waitForSpecificDocument ( expectedDocumentId : string ) : Promise < network . Request | undefined > {
1120
- return new Promise ( ( resolve , reject ) => {
1121
- assert ( ! this . _onSpecificDocument ) ;
1122
- this . _onSpecificDocument = { expectedDocumentId, resolve, reject } ;
1123
- } ) ;
1124
- }
1125
-
1126
- waitForNewDocument ( url ?: types . URLMatch ) : Promise < network . Request | undefined > {
1127
- return new Promise ( ( resolve , reject ) => {
1128
- assert ( ! this . _onNewDocument ) ;
1129
- this . _onNewDocument = { url, resolve, reject } ;
1130
- } ) ;
1131
- }
1132
-
1133
- waitForLifecycle ( waitUntil : types . LifecycleEvent ) : Promise < void > {
1134
- if ( waitUntil as unknown === 'networkidle0' )
1135
- waitUntil = 'networkidle' ;
1136
- if ( ! types . kLifecycleEvents . has ( waitUntil ) )
1137
- throw new Error ( `Unsupported waitUntil option ${ String ( waitUntil ) } ` ) ;
1138
- if ( this . _frame . _subtreeLifecycleEvents . has ( waitUntil ) )
1139
- return Promise . resolve ( ) ;
1140
- return new Promise ( resolve => {
1141
- assert ( ! this . _onLifecycle ) ;
1142
- this . _onLifecycle = { waitUntil, resolve } ;
1143
- } ) ;
1144
- }
1145
-
1146
- done ( ) {
1147
- this . _frame . _frameTasks . delete ( this ) ;
1148
- }
1149
- }
1150
-
1151
1096
async function runNavigationTask < T > ( frame : Frame , options : types . TimeoutOptions , apiName : string , task : ( progress : Progress ) => Promise < T > ) : Promise < T > {
1152
1097
const page = frame . _page ;
1153
1098
const controller = new ProgressController ( page . _logger , page . _timeoutSettings . navigationTimeout ( options ) , apiName ) ;
@@ -1156,3 +1101,11 @@ async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions,
1156
1101
frame . _detachedPromise . then ( ( ) => controller . abort ( new Error ( 'Navigating frame was detached!' ) ) ) ;
1157
1102
return controller . run ( task ) ;
1158
1103
}
1104
+
1105
+ function verifyLifecycle ( waitUntil : types . LifecycleEvent ) : types . LifecycleEvent {
1106
+ if ( waitUntil as unknown === 'networkidle0' )
1107
+ waitUntil = 'networkidle' ;
1108
+ if ( ! types . kLifecycleEvents . has ( waitUntil ) )
1109
+ throw new Error ( `Unsupported waitUntil option ${ String ( waitUntil ) } ` ) ;
1110
+ return waitUntil ;
1111
+ }
0 commit comments