Skip to content

Commit 89b5d2f

Browse files
authored
fix(setContent): manually reset lifecycyle for all browsers at the right moment (#679)
1 parent aa2ecde commit 89b5d2f

File tree

6 files changed

+51
-49
lines changed

6 files changed

+51
-49
lines changed

src/chromium/crPage.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,8 @@ export class CRPage implements PageDelegate {
130130
return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId };
131131
}
132132

133-
needsLifecycleResetOnSetContent(): boolean {
134-
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
135-
// lifecycle event. @see https://crrev.com/608658
136-
return false;
137-
}
138-
139133
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
140-
if (event.name === 'init')
141-
this._page._frameManager.frameLifecycleEvent(event.frameId, 'clear');
142-
else if (event.name === 'load')
134+
if (event.name === 'load')
143135
this._page._frameManager.frameLifecycleEvent(event.frameId, 'load');
144136
else if (event.name === 'DOMContentLoaded')
145137
this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');

src/firefox/ffPage.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,6 @@ export class FFPage implements PageDelegate {
260260
return { newDocumentId: response.navigationId || undefined, isSameDocument: !response.navigationId };
261261
}
262262

263-
needsLifecycleResetOnSetContent(): boolean {
264-
return true;
265-
}
266-
267263
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
268264
const array = [];
269265
for (const [name, value] of Object.entries(headers))

src/frames.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'net
5454
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
5555

5656
export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' };
57+
type ConsoleTagHandler = () => void;
5758

5859
export class FrameManager {
5960
private _page: Page;
6061
private _frames = new Map<string, Frame>();
6162
private _webSockets = new Map<string, network.WebSocket>();
6263
private _mainFrame: Frame;
6364
readonly _lifecycleWatchers = new Set<LifecycleWatcher>();
65+
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
6466

6567
constructor(page: Page) {
6668
this._page = page;
@@ -116,8 +118,7 @@ export class FrameManager {
116118
frame._url = url;
117119
frame._name = name;
118120
frame._lastDocumentId = documentId;
119-
this.frameLifecycleEvent(frameId, 'clear');
120-
this.clearInflightRequests(frame);
121+
this.clearFrameLifecycle(frame);
121122
this.clearWebSockets(frame);
122123
if (!initial) {
123124
for (const watcher of this._lifecycleWatchers)
@@ -158,24 +159,21 @@ export class FrameManager {
158159
this._page.emit(Events.Page.Load);
159160
}
160161

161-
frameLifecycleEvent(frameId: string, event: LifecycleEvent | 'clear') {
162+
frameLifecycleEvent(frameId: string, event: LifecycleEvent) {
162163
const frame = this._frames.get(frameId);
163164
if (!frame)
164165
return;
165-
if (event === 'clear') {
166-
frame._firedLifecycleEvents.clear();
167-
} else {
168-
frame._firedLifecycleEvents.add(event);
169-
for (const watcher of this._lifecycleWatchers)
170-
watcher._onLifecycleEvent(frame);
171-
}
166+
frame._firedLifecycleEvents.add(event);
167+
for (const watcher of this._lifecycleWatchers)
168+
watcher._onLifecycleEvent(frame);
172169
if (frame === this._mainFrame && event === 'load')
173170
this._page.emit(Events.Page.Load);
174171
if (frame === this._mainFrame && event === 'domcontentloaded')
175172
this._page.emit(Events.Page.DOMContentLoaded);
176173
}
177174

178-
clearInflightRequests(frame: Frame) {
175+
clearFrameLifecycle(frame: Frame) {
176+
frame._firedLifecycleEvents.clear();
179177
// Keep the current navigation request if any.
180178
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId));
181179
this._stopNetworkIdleTimer(frame, 'networkidle0');
@@ -330,6 +328,18 @@ export class FrameManager {
330328
clearTimeout(timeoutId);
331329
frame._networkIdleTimers.delete(event);
332330
}
331+
332+
interceptConsoleMessage(message: ConsoleMessage): boolean {
333+
if (message.type() !== 'debug')
334+
return false;
335+
const tag = message.text();
336+
const handler = this._consoleMessageTags.get(tag);
337+
if (!handler)
338+
return false;
339+
this._consoleMessageTags.delete(tag);
340+
handler();
341+
return true;
342+
}
333343
}
334344

335345
export class Frame {
@@ -345,6 +355,7 @@ export class Frame {
345355
_name = '';
346356
_inflightRequests = new Set<network.Request>();
347357
readonly _networkIdleTimers = new Map<LifecycleEvent, NodeJS.Timer>();
358+
private _setContentCounter = 0;
348359

349360
constructor(page: Page, id: string, parentFrame: Frame | null) {
350361
this._id = id;
@@ -510,23 +521,27 @@ export class Frame {
510521
}
511522

512523
async setContent(html: string, options?: NavigateOptions): Promise<void> {
524+
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
513525
const context = await this._utilityContext();
514-
if (this._page._delegate.needsLifecycleResetOnSetContent()) {
515-
this._page._frameManager.frameLifecycleEvent(this._id, 'clear');
516-
this._page._frameManager.clearInflightRequests(this);
517-
}
518-
await context.evaluate(html => {
526+
let watcher: LifecycleWatcher;
527+
this._page._frameManager._consoleMessageTags.set(tag, () => {
528+
// Clear lifecycle right after document.open() - see 'tag' below.
529+
this._page._frameManager.clearFrameLifecycle(this);
530+
watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
531+
});
532+
await context.evaluate((html, tag) => {
519533
window.stop();
520534
document.open();
535+
console.debug(tag); // eslint-disable-line no-console
521536
document.write(html);
522537
document.close();
523-
}, html);
524-
const watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
538+
}, html, tag);
539+
assert(watcher!, 'Was not able to clear lifecycle in setContent');
525540
const error = await Promise.race([
526-
watcher.timeoutOrTerminationPromise,
527-
watcher.lifecyclePromise,
541+
watcher!.timeoutOrTerminationPromise,
542+
watcher!.lifecyclePromise,
528543
]);
529-
watcher.dispose();
544+
watcher!.dispose();
530545
if (error)
531546
throw error;
532547
}
@@ -1092,4 +1107,4 @@ function selectorToString(selector: string, visibility: types.Visibility): strin
10921107
label = ''; break;
10931108
}
10941109
return `${label}${selector}`;
1095-
}
1110+
}

src/page.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export interface PageDelegate {
4343
closePage(runBeforeUnload: boolean): Promise<void>;
4444

4545
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
46-
needsLifecycleResetOnSetContent(): boolean;
4746

4847
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
4948
setViewport(viewport: types.Viewport): Promise<void>;
@@ -296,11 +295,12 @@ export class Page extends platform.EventEmitter {
296295
}
297296

298297
_addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) {
299-
if (!this.listenerCount(Events.Page.Console)) {
298+
const message = new ConsoleMessage(type, text, args, location);
299+
const intercepted = this._frameManager.interceptConsoleMessage(message);
300+
if (intercepted || !this.listenerCount(Events.Page.Console))
300301
args.forEach(arg => arg.dispose());
301-
return;
302-
}
303-
this.emit(Events.Page.Console, new ConsoleMessage(type, text, args, location));
302+
else
303+
this.emit(Events.Page.Console, message);
304304
}
305305

306306
url(): string {

src/webkit/wkPage.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class WKPage implements PageDelegate {
4848
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
4949
private readonly _workers: WKWorkers;
5050
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
51+
private _mainFrameContextId?: number;
5152
private _sessionListeners: RegisteredListener[] = [];
5253
private readonly _bootstrapScripts: string[] = [];
5354

@@ -287,6 +288,8 @@ export class WKPage implements PageDelegate {
287288
frame._contextCreated('main', context);
288289
else if (contextPayload.name === UTILITY_WORLD_NAME)
289290
frame._contextCreated('utility', context);
291+
if (contextPayload.isPageContext && frame === this._page.mainFrame())
292+
this._mainFrameContextId = contextPayload.id;
290293
this._contextIdToContext.set(contextPayload.id, context);
291294
}
292295

@@ -298,11 +301,9 @@ export class WKPage implements PageDelegate {
298301
return { newDocumentId: result.loaderId, isSameDocument: !result.loaderId };
299302
}
300303

301-
needsLifecycleResetOnSetContent(): boolean {
302-
return true;
303-
}
304-
305-
private async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
304+
private _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
305+
// Note: do no introduce await in this function, otherwise we lose the ordering.
306+
// For example, frame.setContent relies on this.
306307
const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message;
307308
if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) {
308309
const parsedObjectId = JSON.parse(parameters[1].objectId!);
@@ -323,14 +324,13 @@ export class WKPage implements PageDelegate {
323324
else if (type === 'timing')
324325
derivedType = 'timeEnd';
325326

326-
const mainFrameContext = await this._page.mainFrame()._mainContext();
327327
const handles = (parameters || []).map(p => {
328328
let context: dom.FrameExecutionContext | null = null;
329329
if (p.objectId) {
330330
const objectId = JSON.parse(p.objectId);
331331
context = this._contextIdToContext.get(objectId.injectedScriptId)!;
332332
} else {
333-
context = mainFrameContext;
333+
context = this._contextIdToContext.get(this._mainFrameContextId!)!;
334334
}
335335
return context._createHandle(p);
336336
});

test/page.spec.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
534534
const result = await page.content();
535535
expect(result).toBe(expectedOutput);
536536
});
537-
it.skip(FFOX || WEBKIT)('should not confuse with previous navigation', async({page, server}) => {
538-
// TODO: ffox and webkit lack 'init' lifecycle event.
537+
it('should not confuse with previous navigation', async({page, server}) => {
539538
const imgPath = '/img.png';
540539
let imgResponse = null;
541540
server.setRoute(imgPath, (req, res) => imgResponse = res);

0 commit comments

Comments
 (0)