fix(setContent): manually reset lifecycyle for all browsers at the right moment (#679)

This commit is contained in:
Dmitry Gozman 2020-01-27 16:51:52 -08:00 committed by GitHub
parent aa2ecde20f
commit 89b5d2f7be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 51 additions and 49 deletions

View file

@ -130,16 +130,8 @@ export class CRPage implements PageDelegate {
return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId }; return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId };
} }
needsLifecycleResetOnSetContent(): boolean {
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
return false;
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) { _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
if (event.name === 'init') if (event.name === 'load')
this._page._frameManager.frameLifecycleEvent(event.frameId, 'clear');
else if (event.name === 'load')
this._page._frameManager.frameLifecycleEvent(event.frameId, 'load'); this._page._frameManager.frameLifecycleEvent(event.frameId, 'load');
else if (event.name === 'DOMContentLoaded') else if (event.name === 'DOMContentLoaded')
this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded'); this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');

View file

@ -260,10 +260,6 @@ export class FFPage implements PageDelegate {
return { newDocumentId: response.navigationId || undefined, isSameDocument: !response.navigationId }; return { newDocumentId: response.navigationId || undefined, isSameDocument: !response.navigationId };
} }
needsLifecycleResetOnSetContent(): boolean {
return true;
}
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
const array = []; const array = [];
for (const [name, value] of Object.entries(headers)) for (const [name, value] of Object.entries(headers))

View file

@ -54,6 +54,7 @@ export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'net
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']); const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' }; export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' };
type ConsoleTagHandler = () => void;
export class FrameManager { export class FrameManager {
private _page: Page; private _page: Page;
@ -61,6 +62,7 @@ export class FrameManager {
private _webSockets = new Map<string, network.WebSocket>(); private _webSockets = new Map<string, network.WebSocket>();
private _mainFrame: Frame; private _mainFrame: Frame;
readonly _lifecycleWatchers = new Set<LifecycleWatcher>(); readonly _lifecycleWatchers = new Set<LifecycleWatcher>();
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
constructor(page: Page) { constructor(page: Page) {
this._page = page; this._page = page;
@ -116,8 +118,7 @@ export class FrameManager {
frame._url = url; frame._url = url;
frame._name = name; frame._name = name;
frame._lastDocumentId = documentId; frame._lastDocumentId = documentId;
this.frameLifecycleEvent(frameId, 'clear'); this.clearFrameLifecycle(frame);
this.clearInflightRequests(frame);
this.clearWebSockets(frame); this.clearWebSockets(frame);
if (!initial) { if (!initial) {
for (const watcher of this._lifecycleWatchers) for (const watcher of this._lifecycleWatchers)
@ -158,24 +159,21 @@ export class FrameManager {
this._page.emit(Events.Page.Load); this._page.emit(Events.Page.Load);
} }
frameLifecycleEvent(frameId: string, event: LifecycleEvent | 'clear') { frameLifecycleEvent(frameId: string, event: LifecycleEvent) {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (!frame) if (!frame)
return; return;
if (event === 'clear') { frame._firedLifecycleEvents.add(event);
frame._firedLifecycleEvents.clear(); for (const watcher of this._lifecycleWatchers)
} else { watcher._onLifecycleEvent(frame);
frame._firedLifecycleEvents.add(event);
for (const watcher of this._lifecycleWatchers)
watcher._onLifecycleEvent(frame);
}
if (frame === this._mainFrame && event === 'load') if (frame === this._mainFrame && event === 'load')
this._page.emit(Events.Page.Load); this._page.emit(Events.Page.Load);
if (frame === this._mainFrame && event === 'domcontentloaded') if (frame === this._mainFrame && event === 'domcontentloaded')
this._page.emit(Events.Page.DOMContentLoaded); this._page.emit(Events.Page.DOMContentLoaded);
} }
clearInflightRequests(frame: Frame) { clearFrameLifecycle(frame: Frame) {
frame._firedLifecycleEvents.clear();
// Keep the current navigation request if any. // Keep the current navigation request if any.
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId)); frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId));
this._stopNetworkIdleTimer(frame, 'networkidle0'); this._stopNetworkIdleTimer(frame, 'networkidle0');
@ -330,6 +328,18 @@ export class FrameManager {
clearTimeout(timeoutId); clearTimeout(timeoutId);
frame._networkIdleTimers.delete(event); frame._networkIdleTimers.delete(event);
} }
interceptConsoleMessage(message: ConsoleMessage): boolean {
if (message.type() !== 'debug')
return false;
const tag = message.text();
const handler = this._consoleMessageTags.get(tag);
if (!handler)
return false;
this._consoleMessageTags.delete(tag);
handler();
return true;
}
} }
export class Frame { export class Frame {
@ -345,6 +355,7 @@ export class Frame {
_name = ''; _name = '';
_inflightRequests = new Set<network.Request>(); _inflightRequests = new Set<network.Request>();
readonly _networkIdleTimers = new Map<LifecycleEvent, NodeJS.Timer>(); readonly _networkIdleTimers = new Map<LifecycleEvent, NodeJS.Timer>();
private _setContentCounter = 0;
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
this._id = id; this._id = id;
@ -510,23 +521,27 @@ export class Frame {
} }
async setContent(html: string, options?: NavigateOptions): Promise<void> { async setContent(html: string, options?: NavigateOptions): Promise<void> {
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext(); const context = await this._utilityContext();
if (this._page._delegate.needsLifecycleResetOnSetContent()) { let watcher: LifecycleWatcher;
this._page._frameManager.frameLifecycleEvent(this._id, 'clear'); this._page._frameManager._consoleMessageTags.set(tag, () => {
this._page._frameManager.clearInflightRequests(this); // Clear lifecycle right after document.open() - see 'tag' below.
} this._page._frameManager.clearFrameLifecycle(this);
await context.evaluate(html => { watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
});
await context.evaluate((html, tag) => {
window.stop(); window.stop();
document.open(); document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html); document.write(html);
document.close(); document.close();
}, html); }, html, tag);
const watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */); assert(watcher!, 'Was not able to clear lifecycle in setContent');
const error = await Promise.race([ const error = await Promise.race([
watcher.timeoutOrTerminationPromise, watcher!.timeoutOrTerminationPromise,
watcher.lifecyclePromise, watcher!.lifecyclePromise,
]); ]);
watcher.dispose(); watcher!.dispose();
if (error) if (error)
throw error; throw error;
} }

View file

@ -43,7 +43,6 @@ export interface PageDelegate {
closePage(runBeforeUnload: boolean): Promise<void>; closePage(runBeforeUnload: boolean): Promise<void>;
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>; navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
needsLifecycleResetOnSetContent(): boolean;
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>; setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
setViewport(viewport: types.Viewport): Promise<void>; setViewport(viewport: types.Viewport): Promise<void>;
@ -296,11 +295,12 @@ export class Page extends platform.EventEmitter {
} }
_addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) {
if (!this.listenerCount(Events.Page.Console)) { const message = new ConsoleMessage(type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Events.Page.Console))
args.forEach(arg => arg.dispose()); args.forEach(arg => arg.dispose());
return; else
} this.emit(Events.Page.Console, message);
this.emit(Events.Page.Console, new ConsoleMessage(type, text, args, location));
} }
url(): string { url(): string {

View file

@ -48,6 +48,7 @@ export class WKPage implements PageDelegate {
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>(); private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
private readonly _workers: WKWorkers; private readonly _workers: WKWorkers;
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>; private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
private _mainFrameContextId?: number;
private _sessionListeners: RegisteredListener[] = []; private _sessionListeners: RegisteredListener[] = [];
private readonly _bootstrapScripts: string[] = []; private readonly _bootstrapScripts: string[] = [];
@ -287,6 +288,8 @@ export class WKPage implements PageDelegate {
frame._contextCreated('main', context); frame._contextCreated('main', context);
else if (contextPayload.name === UTILITY_WORLD_NAME) else if (contextPayload.name === UTILITY_WORLD_NAME)
frame._contextCreated('utility', context); frame._contextCreated('utility', context);
if (contextPayload.isPageContext && frame === this._page.mainFrame())
this._mainFrameContextId = contextPayload.id;
this._contextIdToContext.set(contextPayload.id, context); this._contextIdToContext.set(contextPayload.id, context);
} }
@ -298,11 +301,9 @@ export class WKPage implements PageDelegate {
return { newDocumentId: result.loaderId, isSameDocument: !result.loaderId }; return { newDocumentId: result.loaderId, isSameDocument: !result.loaderId };
} }
needsLifecycleResetOnSetContent(): boolean { private _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
return true; // Note: do no introduce await in this function, otherwise we lose the ordering.
} // For example, frame.setContent relies on this.
private async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message; const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message;
if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) { if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) {
const parsedObjectId = JSON.parse(parameters[1].objectId!); const parsedObjectId = JSON.parse(parameters[1].objectId!);
@ -323,14 +324,13 @@ export class WKPage implements PageDelegate {
else if (type === 'timing') else if (type === 'timing')
derivedType = 'timeEnd'; derivedType = 'timeEnd';
const mainFrameContext = await this._page.mainFrame()._mainContext();
const handles = (parameters || []).map(p => { const handles = (parameters || []).map(p => {
let context: dom.FrameExecutionContext | null = null; let context: dom.FrameExecutionContext | null = null;
if (p.objectId) { if (p.objectId) {
const objectId = JSON.parse(p.objectId); const objectId = JSON.parse(p.objectId);
context = this._contextIdToContext.get(objectId.injectedScriptId)!; context = this._contextIdToContext.get(objectId.injectedScriptId)!;
} else { } else {
context = mainFrameContext; context = this._contextIdToContext.get(this._mainFrameContextId!)!;
} }
return context._createHandle(p); return context._createHandle(p);
}); });

View file

@ -534,8 +534,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
const result = await page.content(); const result = await page.content();
expect(result).toBe(expectedOutput); expect(result).toBe(expectedOutput);
}); });
it.skip(FFOX || WEBKIT)('should not confuse with previous navigation', async({page, server}) => { it('should not confuse with previous navigation', async({page, server}) => {
// TODO: ffox and webkit lack 'init' lifecycle event.
const imgPath = '/img.png'; const imgPath = '/img.png';
let imgResponse = null; let imgResponse = null;
server.setRoute(imgPath, (req, res) => imgResponse = res); server.setRoute(imgPath, (req, res) => imgResponse = res);