Skip to content

Commit 2a86ead

Browse files
authored
chore: replace FrameTask with internal events on Frame (#2856)
We now use a few helper.waitForEvent calls to wait for internal events kNavigationEvent and kLifecycleEvent. With these events, we should be able to replicate logic over rpc.
1 parent 35cb20d commit 2a86ead

File tree

7 files changed

+124
-159
lines changed

7 files changed

+124
-159
lines changed

src/browserContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
9999
const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options), 'browserContext.waitForEvent');
100100
if (event !== Events.BrowserContext.Close)
101101
this._closePromise.then(error => progressController.abort(error));
102-
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
102+
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate).promise);
103103
}
104104

105105
_browserClosed() {

src/frames.ts

Lines changed: 85 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { selectors } from './selectors';
2828
import * as types from './types';
2929
import { BrowserContext } from './browserContext';
3030
import { Progress, ProgressController } from './progress';
31+
import { EventEmitter } from 'events';
3132

3233
type ContextData = {
3334
contextPromise: Promise<dom.FrameExecutionContext>;
@@ -51,6 +52,13 @@ type ConsoleTagHandler = () => void;
5152

5253
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
5354

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+
5462
export class FrameManager {
5563
private _page: Page;
5664
private _frames = new Map<string, Frame>();
@@ -158,20 +166,23 @@ export class FrameManager {
158166
else
159167
frame._currentDocument = { documentId, request: undefined };
160168
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);
163171
frame._onClearLifecycle();
164-
if (!initial)
172+
if (!initial) {
173+
this._page._logger.info(` navigated to "${url}"`);
165174
this._page.emit(Events.Page.FrameNavigated, frame);
175+
}
166176
}
167177

168178
frameCommittedSameDocumentNavigation(frameId: string, url: string) {
169179
const frame = this._frames.get(frameId);
170180
if (!frame)
171181
return;
172182
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}"`);
175186
this._page.emit(Events.Page.FrameNavigated, frame);
176187
}
177188

@@ -181,10 +192,9 @@ export class FrameManager {
181192
return;
182193
if (documentId !== undefined && frame._pendingDocument.documentId !== documentId)
183194
return;
184-
const pending = frame._pendingDocument;
195+
const navigationEvent: NavigationEvent = { documentInfo: frame._pendingDocument, error: new Error(errorText) };
185196
frame._pendingDocument = undefined;
186-
for (const task of frame._frameTasks)
187-
task.onNewDocument(pending, new Error(errorText));
197+
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
188198
}
189199

190200
frameDetached(frameId: string) {
@@ -288,12 +298,12 @@ export class FrameManager {
288298
}
289299

290300
export class Frame {
301+
readonly _eventEmitter: EventEmitter;
291302
_id: string;
292303
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
293304
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
294305
_currentDocument: DocumentInfo;
295306
_pendingDocument?: DocumentInfo;
296-
_frameTasks = new Set<FrameTask>();
297307
readonly _page: Page;
298308
private _parentFrame: Frame | null;
299309
_url = '';
@@ -308,6 +318,8 @@ export class Frame {
308318
private _detachedCallback = () => {};
309319

310320
constructor(page: Page, id: string, parentFrame: Frame | null) {
321+
this._eventEmitter = new EventEmitter();
322+
this._eventEmitter.setMaxListeners(0);
311323
this._id = id;
312324
this._page = page;
313325
this._parentFrame = parentFrame;
@@ -363,8 +375,9 @@ export class Frame {
363375
for (const event of events) {
364376
// Checking whether we have already notified about this event.
365377
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`);
368381
if (this === mainFrame && event === 'load')
369382
this._page.emit(Events.Page.Load);
370383
if (this === mainFrame && event === 'domcontentloaded')
@@ -376,7 +389,8 @@ export class Frame {
376389

377390
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
378391
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}"`);
380394
const headers = (this._page._state.extraHTTPHeaders || {});
381395
let referer = headers['referer'] || headers['Referer'];
382396
if (options.referer !== undefined) {
@@ -386,39 +400,57 @@ export class Frame {
386400
}
387401
url = helper.completeUserURL(url);
388402

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;
397407
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;
401422
} else {
402-
await sameDocumentPromise;
423+
event = await sameDocument.promise;
403424
}
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;
406430
return request ? request._finalRequest().response() : null;
407431
});
408432
}
409433

410434
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
411435
return runNavigationTask(this, options, this._apiName('waitForNavigation'), async progress => {
412436
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;
422454
return request ? request._finalRequest().response() : null;
423455
});
424456
}
@@ -428,9 +460,9 @@ export class Frame {
428460
}
429461

430462
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;
434466
}
435467

436468
async frameElement(): Promise<dom.ElementHandle> {
@@ -1035,15 +1067,18 @@ class SignalBarrier {
10351067

10361068
async addFrameNavigation(frame: Frame) {
10371069
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+
});
10391075
await Promise.race([
10401076
frame._page._disconnectedPromise,
10411077
frame._page._crashedPromise,
10421078
frame._detachedPromise,
1043-
frameTask.waitForNewDocument(),
1044-
frameTask.waitForSameDocumentNavigation(),
1079+
waiter.promise,
10451080
]).catch(e => {});
1046-
frameTask.done();
1081+
waiter.dispose();
10471082
this.release();
10481083
}
10491084

@@ -1058,96 +1093,6 @@ class SignalBarrier {
10581093
}
10591094
}
10601095

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-
11511096
async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions, apiName: string, task: (progress: Progress) => Promise<T>): Promise<T> {
11521097
const page = frame._page;
11531098
const controller = new ProgressController(page._logger, page._timeoutSettings.navigationTimeout(options), apiName);
@@ -1156,3 +1101,11 @@ async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions,
11561101
frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
11571102
return controller.run(task);
11581103
}
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+
}

src/helper.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,23 +296,25 @@ class Helper {
296296
}));
297297
}
298298

299-
static async waitForEvent(progress: Progress, emitter: EventEmitter, event: string, predicate?: Function): Promise<any> {
299+
static waitForEvent(progress: Progress | null, emitter: EventEmitter, event: string | symbol, predicate?: Function): { promise: Promise<any>, dispose: () => void } {
300300
const listeners: RegisteredListener[] = [];
301301
const promise = new Promise((resolve, reject) => {
302302
listeners.push(helper.addEventListener(emitter, event, eventArg => {
303303
try {
304304
if (predicate && !predicate(eventArg))
305305
return;
306+
helper.removeEventListeners(listeners);
306307
resolve(eventArg);
307308
} catch (e) {
309+
helper.removeEventListeners(listeners);
308310
reject(e);
309311
}
310312
}));
311313
});
312-
progress.cleanupWhenAborted(() => helper.removeEventListeners(listeners));
313-
const result = await promise;
314-
helper.removeEventListeners(listeners);
315-
return result;
314+
const dispose = () => helper.removeEventListeners(listeners);
315+
if (progress)
316+
progress.cleanupWhenAborted(dispose);
317+
return { promise, dispose };
316318
}
317319

318320
static isDebugMode(): boolean {

src/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export class Page extends EventEmitter {
356356
this._disconnectedPromise.then(error => progressController.abort(error));
357357
if (event !== Events.Page.Crash)
358358
this._crashedPromise.then(error => progressController.abort(error));
359-
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
359+
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate).promise);
360360
}
361361

362362
async goBack(options?: types.NavigateOptions): Promise<network.Response | null> {

0 commit comments

Comments
 (0)