fix(navigations): remove LifecycleWatcher, fix flakes (#882)

This commit is contained in:
Joel Einbinder 2020-02-10 18:35:47 -08:00 committed by GitHub
parent c03e8b7946
commit 251ad38824
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 286 additions and 264 deletions

View File

@ -128,7 +128,7 @@ export class CRPage implements PageDelegate {
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id });
if (response.errorText)
throw new Error(`${response.errorText} at ${url}`);
return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId };
return { newDocumentId: response.loaderId };
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {

View File

@ -134,8 +134,8 @@ export class FFPage implements PageDelegate {
_onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) {
const frame = this._page._frameManager.frame(params.frameId)!;
for (const watcher of this._page._frameManager._lifecycleWatchers)
watcher._onAbortedNewDocumentNavigation(frame, params.navigationId, params.errorText);
for (const watcher of frame._documentWatchers)
watcher(params.navigationId, new Error(params.errorText));
}
_onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) {
@ -256,7 +256,7 @@ export class FFPage implements PageDelegate {
async navigateFrame(frame: frames.Frame, url: string, referer: string | undefined): Promise<frames.GotoResult> {
const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id });
return { newDocumentId: response.navigationId || undefined, isSameDocument: !response.navigationId };
return { newDocumentId: response.navigationId || undefined };
}
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {

View File

@ -47,7 +47,6 @@ export type GotoOptions = NavigateOptions & {
};
export type GotoResult = {
newDocumentId?: string,
isSameDocument?: boolean,
};
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
@ -61,7 +60,7 @@ export class FrameManager {
private _frames = new Map<string, Frame>();
private _webSockets = new Map<string, network.WebSocket>();
private _mainFrame: Frame;
readonly _lifecycleWatchers = new Set<LifecycleWatcher>();
readonly _lifecycleWatchers = new Set<() => void>();
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
constructor(page: Page) {
@ -118,13 +117,12 @@ export class FrameManager {
frame._url = url;
frame._name = name;
frame._lastDocumentId = documentId;
for (const watcher of frame._documentWatchers)
watcher(documentId);
this.clearFrameLifecycle(frame);
this.clearWebSockets(frame);
if (!initial) {
for (const watcher of this._lifecycleWatchers)
watcher._onCommittedNewDocumentNavigation(frame);
if (!initial)
this._page.emit(Events.Page.FrameNavigated, frame);
}
}
frameCommittedSameDocumentNavigation(frameId: string, url: string) {
@ -132,8 +130,8 @@ export class FrameManager {
if (!frame)
return;
frame._url = url;
for (const watcher of this._lifecycleWatchers)
watcher._onNavigatedWithinDocument(frame);
for (const watcher of frame._sameDocumentNavigationWatchers)
watcher();
this._page.emit(Events.Page.FrameNavigated, frame);
}
@ -152,7 +150,7 @@ export class FrameManager {
frame._firedLifecycleEvents.add('domcontentloaded');
frame._firedLifecycleEvents.add('load');
for (const watcher of this._lifecycleWatchers)
watcher._onLifecycleEvent(frame);
watcher();
if (frame === this.mainFrame() && !hasDOMContentLoaded)
this._page.emit(Events.Page.DOMContentLoaded);
if (frame === this.mainFrame() && !hasLoad)
@ -165,7 +163,7 @@ export class FrameManager {
return;
frame._firedLifecycleEvents.add(event);
for (const watcher of this._lifecycleWatchers)
watcher._onLifecycleEvent(frame);
watcher();
if (frame === this._mainFrame && event === 'load')
this._page.emit(Events.Page.Load);
if (frame === this._mainFrame && event === 'domcontentloaded')
@ -194,9 +192,9 @@ export class FrameManager {
requestStarted(request: network.Request) {
this._inflightRequestStarted(request);
const frame = request.frame();
if (request._documentId && frame && !request.redirectChain().length) {
for (const watcher of this._lifecycleWatchers)
watcher._onNavigationRequest(frame, request);
if (frame) {
for (const watcher of frame._requestWatchers)
watcher(request);
}
if (!request._isFavicon)
this._page._requestStarted(request);
@ -222,8 +220,8 @@ export class FrameManager {
let errorText = request.failure()!.errorText;
if (canceled)
errorText += '; maybe frame was detached?';
for (const watcher of this._lifecycleWatchers)
watcher._onAbortedNewDocumentNavigation(frame, request._documentId, errorText);
for (const watcher of frame._documentWatchers)
watcher(request._documentId, new Error(errorText));
}
}
if (!request._isFavicon)
@ -274,9 +272,9 @@ export class FrameManager {
ws._error(errorMessage);
}
provisionalLoadFailed(documentId: string, error: string) {
for (const watcher of this._lifecycleWatchers)
watcher._onProvisionalLoadFailed(documentId, error);
provisionalLoadFailed(frame: Frame, documentId: string, error: string) {
for (const watcher of frame._documentWatchers)
watcher(documentId, new Error(error));
}
private _removeFramesRecursively(frame: Frame) {
@ -284,8 +282,6 @@ export class FrameManager {
this._removeFramesRecursively(child);
frame._onDetached();
this._frames.delete(frame._id);
for (const watcher of this._lifecycleWatchers)
watcher._onFrameDetached(frame);
this._page.emit(Events.Page.FrameDetached, frame);
}
@ -345,7 +341,10 @@ export class FrameManager {
export class Frame {
_id: string;
readonly _firedLifecycleEvents: Set<LifecycleEvent>;
_lastDocumentId: string;
_lastDocumentId = '';
_requestWatchers = new Set<(request: network.Request) => void>();
_documentWatchers = new Set<(documentId: string, error?: Error) => void>();
_sameDocumentNavigationWatchers = new Set<() => void>();
readonly _page: Page;
private _parentFrame: Frame | null;
_url = '';
@ -356,14 +355,17 @@ export class Frame {
_inflightRequests = new Set<network.Request>();
readonly _networkIdleTimers = new Map<LifecycleEvent, NodeJS.Timer>();
private _setContentCounter = 0;
private _detachedPromise: Promise<void>;
private _detachedCallback = () => {};
constructor(page: Page, id: string, parentFrame: Frame | null) {
this._id = id;
this._firedLifecycleEvents = new Set();
this._lastDocumentId = '';
this._page = page;
this._parentFrame = parentFrame;
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
this._contextData.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._contextData.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._setContext('main', null);
@ -373,15 +375,21 @@ export class Frame {
this._parentFrame._childFrames.add(this);
}
async goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
let referer = (this._page._state.extraHTTPHeaders || {})['referer'];
if (options && options.referer !== undefined) {
if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header');
referer = options.referer;
}
const watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
url = helper.completeUserURL(url);
const { timeout = this._page._timeoutSettings.navigationTimeout() } = options;
const disposer = new Disposer();
const timeoutPromise = disposer.add(createTimeoutPromise(timeout));
const frameDestroyedPromise = this._createFrameDestroyedPromise();
const sameDocumentPromise = disposer.add(this._waitForSameDocumentNavigation());
const requestWatcher = disposer.add(this._trackDocumentRequests());
let navigateResult: GotoResult;
const navigate = async () => {
try {
@ -391,52 +399,175 @@ export class Frame {
}
};
let error = await Promise.race([
throwIfError(await Promise.race([
navigate(),
watcher.timeoutOrTerminationPromise,
]);
if (!error) {
const promises = [watcher.timeoutOrTerminationPromise];
if (navigateResult!.newDocumentId) {
watcher.setExpectedDocumentId(navigateResult!.newDocumentId, url);
promises.push(watcher.newDocumentNavigationPromise);
} else if (navigateResult!.isSameDocument) {
promises.push(watcher.sameDocumentNavigationPromise);
} else {
promises.push(watcher.sameDocumentNavigationPromise, watcher.newDocumentNavigationPromise);
}
error = await Promise.race(promises);
timeoutPromise,
frameDestroyedPromise,
]));
const promises: Promise<Error|void>[] = [timeoutPromise, frameDestroyedPromise];
if (navigateResult!.newDocumentId)
promises.push(disposer.add(this._waitForSpecificDocument(navigateResult!.newDocumentId)));
else
promises.push(sameDocumentPromise);
throwIfError(await Promise.race(promises));
const request = (navigateResult! && navigateResult!.newDocumentId) ? requestWatcher.get(navigateResult!.newDocumentId) : null;
const waitForLifecyclePromise = disposer.add(this._waitForLifecycle(options.waitUntil));
throwIfError(await Promise.race([timeoutPromise, frameDestroyedPromise, waitForLifecyclePromise]));
disposer.dispose();
return request ? request._finalRequest._waitForResponse() : null;
function throwIfError(error: Error|void): asserts error is void {
if (!error)
return;
disposer.dispose();
const message = `While navigating to ${url}: ${error.message}`;
if (error instanceof TimeoutError)
throw new TimeoutError(message);
throw new Error(message);
}
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
async waitForNavigation(options?: WaitForNavigationOptions): Promise<network.Response | null> {
const watcher = new LifecycleWatcher(this, options, true /* supportUrlMatch */);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.sameDocumentNavigationPromise,
watcher.newDocumentNavigationPromise,
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
const disposer = new Disposer();
const requestWatcher = disposer.add(this._trackDocumentRequests());
const {timeout = this._page._timeoutSettings.navigationTimeout()} = options;
const failurePromise = Promise.race([
this._createFrameDestroyedPromise(),
disposer.add(createTimeoutPromise(timeout)),
]);
watcher.dispose();
let documentId: string|null = null;
let error: void|Error = await Promise.race([
failurePromise,
disposer.add(this._waitForNewDocument(options.url)).then(result => {
if (result.error)
return result.error;
documentId = result.documentId;
}),
disposer.add(this._waitForSameDocumentNavigation(options.url)),
]);
const request = requestWatcher.get(documentId!);
if (!error) {
error = await Promise.race([
failurePromise,
disposer.add(this._waitForLifecycle(options.waitUntil)),
]);
}
disposer.dispose();
if (error)
throw error;
return watcher.navigationResponse();
return request ? request._finalRequest._waitForResponse() : null;
}
async waitForLoadState(options?: NavigateOptions): Promise<void> {
const watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
async waitForLoadState(options: NavigateOptions = {}): Promise<void> {
const {timeout = this._page._timeoutSettings.navigationTimeout()} = options;
const disposer = new Disposer();
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.lifecyclePromise
this._createFrameDestroyedPromise(),
disposer.add(createTimeoutPromise(timeout)),
disposer.add(this._waitForLifecycle(options.waitUntil)),
]);
watcher.dispose();
disposer.dispose();
if (error)
throw error;
}
_waitForSpecificDocument(expectedDocumentId: string): Disposable<Promise<Error|void>> {
let resolve: (error: Error|void) => void;
const promise = new Promise<Error|void>(x => resolve = x);
const watch = (documentId: string, error?: Error) => {
if (documentId !== expectedDocumentId)
return resolve(new Error('Navigation interrupted by another one'));
resolve(error);
};
const dispose = () => this._documentWatchers.delete(watch);
this._documentWatchers.add(watch);
return {value: promise, dispose};
}
_waitForNewDocument(url?: types.URLMatch): Disposable<Promise<{error?: Error, documentId: string}>> {
let resolve: (error: {error?: Error, documentId: string}) => void;
const promise = new Promise<{error?: Error, documentId: string}>(x => resolve = x);
const watch = (documentId: string, error?: Error) => {
if (!error && !platform.urlMatches(this.url(), url))
return;
resolve({error, documentId});
};
const dispose = () => this._documentWatchers.delete(watch);
this._documentWatchers.add(watch);
return {value: promise, dispose};
}
_waitForSameDocumentNavigation(url?: types.URLMatch): Disposable<Promise<void>> {
let resolve: () => void;
const promise = new Promise<void>(x => resolve = x);
const watch = () => {
if (platform.urlMatches(this.url(), url))
resolve();
};
const dispose = () => this._sameDocumentNavigationWatchers.delete(watch);
this._sameDocumentNavigationWatchers.add(watch);
return {value: promise, dispose};
}
_waitForLifecycle(waitUntil: LifecycleEvent|LifecycleEvent[] = 'load'): Disposable<Promise<void>> {
let resolve: () => void;
const expectedLifecycle = typeof waitUntil === 'string' ? [waitUntil] : waitUntil;
for (const event of expectedLifecycle) {
if (!kLifecycleEvents.has(event))
throw new Error(`Unsupported waitUntil option ${String(event)}`);
}
const checkLifecycleComplete = () => {
if (!checkLifecycleRecursively(this))
return;
resolve();
};
const promise = new Promise<void>(x => resolve = x);
const dispose = () => this._page._frameManager._lifecycleWatchers.delete(checkLifecycleComplete);
this._page._frameManager._lifecycleWatchers.add(checkLifecycleComplete);
checkLifecycleComplete();
return {value: promise, dispose};
function checkLifecycleRecursively(frame: Frame): boolean {
for (const event of expectedLifecycle) {
if (!frame._firedLifecycleEvents.has(event))
return false;
}
for (const child of frame.childFrames()) {
if (!checkLifecycleRecursively(child))
return false;
}
return true;
}
}
_trackDocumentRequests(): Disposable<Map<string, network.Request>> {
const requestMap = new Map<string, network.Request>();
const dispose = () => {
this._requestWatchers.delete(onRequest);
};
const onRequest = (request: network.Request) => {
if (!request._documentId || request.redirectChain().length)
return;
requestMap.set(request._documentId, request);
};
this._requestWatchers.add(onRequest);
return {dispose, value: requestMap};
}
_createFrameDestroyedPromise(): Promise<Error> {
return Promise.race([
this._page._disconnectedPromise.then(() => new Error('Navigation failed because browser has disconnected!')),
this._detachedPromise.then(() => new Error('Navigating frame was detached!')),
]);
}
async frameElement(): Promise<dom.ElementHandle> {
return this._page._delegate.getFrameElement(this);
}
@ -527,27 +658,21 @@ export class Frame {
async setContent(html: string, options?: NavigateOptions): Promise<void> {
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
let watcher: LifecycleWatcher;
this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
this._page._frameManager.clearFrameLifecycle(this);
watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */);
const lifecyclePromise = new Promise(resolve => {
this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
this._page._frameManager.clearFrameLifecycle(this);
resolve(this.waitForLoadState(options));
});
});
await context.evaluate((html, tag) => {
const contentPromise = context.evaluate((html, tag) => {
window.stop();
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, html, tag);
assert(watcher!, 'Was not able to clear lifecycle in setContent');
const error = await Promise.race([
watcher!.timeoutOrTerminationPromise,
watcher!.lifecyclePromise,
]);
watcher!.dispose();
if (error)
throw error;
await Promise.all([contentPromise, lifecyclePromise]);
}
name(): string {
@ -826,6 +951,7 @@ export class Frame {
_onDetached() {
this._detached = true;
this._detachedCallback();
for (const data of this._contextData.values()) {
for (const rerunnableTask of data.rerunnableTasks)
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
@ -957,162 +1083,37 @@ class RerunnableTask {
}
}
class LifecycleWatcher {
readonly sameDocumentNavigationPromise: Promise<Error | null>;
readonly lifecyclePromise: Promise<void>;
readonly newDocumentNavigationPromise: Promise<Error | null>;
readonly timeoutOrTerminationPromise: Promise<Error | null>;
private _expectedLifecycle: LifecycleEvent[];
private _frame: Frame;
private _navigationRequest: network.Request | null = null;
private _sameDocumentNavigationCompleteCallback: () => void = () => {};
private _lifecycleCallback: () => void = () => {};
private _newDocumentNavigationCompleteCallback: () => void = () => {};
private _frameDetachedCallback: (err: Error) => void = () => {};
private _navigationAbortedCallback: (err: Error) => void = () => {};
private _maximumTimer?: NodeJS.Timer;
private _hasSameDocumentNavigation = false;
private _targetUrl: string | undefined;
private _expectedDocumentId: string | undefined;
private _urlMatch: types.URLMatch | undefined;
constructor(frame: Frame, options: WaitForNavigationOptions | undefined, supportUrlMatch: boolean) {
options = options || {};
let { waitUntil = 'load' as LifecycleEvent } = options;
const { timeout = frame._page._timeoutSettings.navigationTimeout() } = options;
if (!Array.isArray(waitUntil))
waitUntil = [waitUntil];
for (const event of waitUntil) {
if (!kLifecycleEvents.has(event))
throw new Error(`Unsupported waitUntil option ${String(event)}`);
}
if (supportUrlMatch)
this._urlMatch = options.url;
this._expectedLifecycle = waitUntil.slice();
this._frame = frame;
this.sameDocumentNavigationPromise = new Promise(f => this._sameDocumentNavigationCompleteCallback = f);
this.lifecyclePromise = new Promise(f => this._lifecycleCallback = f);
this.newDocumentNavigationPromise = new Promise(f => this._newDocumentNavigationCompleteCallback = f);
this.timeoutOrTerminationPromise = Promise.race([
this._createTimeoutPromise(timeout),
new Promise<Error>(f => this._frameDetachedCallback = f),
new Promise<Error>(f => this._navigationAbortedCallback = f),
this._frame._page._disconnectedPromise.then(() => new Error('Navigation failed because browser has disconnected!')),
]);
frame._page._frameManager._lifecycleWatchers.add(this);
this._checkLifecycleComplete();
type Disposable<T> = {value: T, dispose: () => void};
class Disposer {
private _disposes: (() => void)[] = [];
add<T>({value, dispose}: Disposable<T>) {
this._disposes.push(dispose);
return value;
}
_urlMatches(urlString: string): boolean {
return !this._urlMatch || platform.urlMatches(urlString, this._urlMatch);
}
setExpectedDocumentId(documentId: string, url: string) {
assert(!this._urlMatch, 'Should not have url match when expecting a particular navigation');
this._expectedDocumentId = documentId;
this._targetUrl = url;
if (this._navigationRequest && this._navigationRequest._documentId !== documentId)
this._navigationRequest = null;
}
_onFrameDetached(frame: Frame) {
if (this._frame === frame) {
this._frameDetachedCallback.call(null, new Error('Navigating frame was detached'));
return;
}
this._checkLifecycleComplete();
}
_onNavigatedWithinDocument(frame: Frame) {
if (frame !== this._frame)
return;
this._hasSameDocumentNavigation = true;
this._checkLifecycleComplete();
}
_onNavigationRequest(frame: Frame, request: network.Request) {
assert(request._documentId);
if (frame !== this._frame || !this._urlMatches(request.url()))
return;
if (this._expectedDocumentId === undefined || this._expectedDocumentId === request._documentId) {
this._navigationRequest = request;
this._expectedDocumentId = request._documentId;
this._targetUrl = request.url();
}
}
_onCommittedNewDocumentNavigation(frame: Frame) {
if (frame === this._frame && this._expectedDocumentId !== undefined && this._navigationRequest &&
frame._lastDocumentId !== this._expectedDocumentId) {
this._navigationAbortedCallback(new Error('Navigation to ' + this._targetUrl + ' was canceled by another one'));
return;
}
if (frame === this._frame && this._expectedDocumentId === undefined && this._urlMatches(frame.url())) {
this._expectedDocumentId = frame._lastDocumentId;
this._targetUrl = frame.url();
}
}
_onAbortedNewDocumentNavigation(frame: Frame, documentId: string, errorText: string) {
if (frame === this._frame && documentId === this._expectedDocumentId) {
if (this._targetUrl)
this._navigationAbortedCallback(new Error('Navigation to ' + this._targetUrl + ' failed: ' + errorText));
else
this._navigationAbortedCallback(new Error('Navigation failed: ' + errorText));
}
}
_onProvisionalLoadFailed(documentId: string, error: string) {
this._onAbortedNewDocumentNavigation(this._frame, documentId, error);
}
_onLifecycleEvent(frame: Frame) {
this._checkLifecycleComplete();
}
async navigationResponse(): Promise<network.Response | null> {
return this._navigationRequest ? this._navigationRequest._finalRequest._waitForFinished() : null;
}
private _createTimeoutPromise(timeout: number): Promise<Error | null> {
if (!timeout)
return new Promise(() => {});
const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded';
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, timeout))
.then(() => new TimeoutError(errorMessage));
}
private _checkLifecycleRecursively(frame: Frame, expectedLifecycle: LifecycleEvent[]): boolean {
for (const event of expectedLifecycle) {
if (!frame._firedLifecycleEvents.has(event))
return false;
}
for (const child of frame.childFrames()) {
if (!this._checkLifecycleRecursively(child, expectedLifecycle))
return false;
}
return true;
}
private _checkLifecycleComplete() {
if (!this._checkLifecycleRecursively(this._frame, this._expectedLifecycle))
return;
if (this._urlMatches(this._frame.url())) {
this._lifecycleCallback();
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
}
if (this._frame._lastDocumentId === this._expectedDocumentId)
this._newDocumentNavigationCompleteCallback();
}
dispose() {
this._frame._page._frameManager._lifecycleWatchers.delete(this);
if (this._maximumTimer)
clearTimeout(this._maximumTimer);
for (const dispose of this._disposes)
dispose();
this._disposes = [];
}
}
function createTimeoutPromise(timeout: number): Disposable<Promise<TimeoutError>> {
if (!timeout)
return { value: new Promise(() => {}), dispose: () => void 0 };
let timer: NodeJS.Timer;
const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded';
const promise = new Promise(fulfill => timer = setTimeout(fulfill, timeout))
.then(() => new TimeoutError(errorMessage));
const dispose = () => {
clearTimeout(timer);
};
return {
value: promise,
dispose
};
}
function selectorToString(selector: string, visibility: types.Visibility): string {
let label;
switch (visibility) {

View File

@ -307,7 +307,7 @@ export class WKPage implements PageDelegate {
throw new Error('Target closed');
const pageProxyId = this._pageProxySession.sessionId;
const result = await this._pageProxySession.connection.browserSession.send('Browser.navigate', { url, pageProxyId, frameId: frame._id, referrer });
return { newDocumentId: result.loaderId, isSameDocument: !result.loaderId };
return { newDocumentId: result.loaderId };
}
private _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {

View File

@ -85,7 +85,7 @@ export class WKPageProxy {
let errorText = event.error;
if (errorText.includes('cancelled'))
errorText += '; maybe frame was detached?';
this._wkPage._page._frameManager.provisionalLoadFailed(event.loaderId, errorText);
this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText);
}
async page(): Promise<Page> {

View File

@ -352,22 +352,20 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
it('should not throw when continued after navigation', async({page, server}) => {
await page.route(server.PREFIX + '/one-style.css', () => {});
// For some reason, Firefox issues load event with one outstanding request.
const failed = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e);
const firstNavigation = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e);
const request = await page.waitForRequest(server.PREFIX + '/one-style.css');
await page.goto(server.PREFIX + '/empty.html');
const error = await failed;
expect(error.message).toBe('Navigation to ' + server.PREFIX + '/one-style.html was canceled by another one');
await firstNavigation;
const notAnError = await request.continue().then(() => null).catch(e => e);
expect(notAnError).toBe(null);
});
it('should not throw when continued after cross-process navigation', async({page, server}) => {
await page.route(server.PREFIX + '/one-style.css', () => {});
// For some reason, Firefox issues load event with one outstanding request.
const failed = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e);
const firstNavigation = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e);
const request = await page.waitForRequest(server.PREFIX + '/one-style.css');
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
const error = await failed;
expect(error.message).toBe('Navigation to ' + server.PREFIX + '/one-style.html was canceled by another one');
await firstNavigation;
const notAnError = await request.continue().then(() => null).catch(e => e);
expect(notAnError).toBe(null);
});

View File

@ -145,7 +145,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
await server.waitForRequest('/one-style.css');
await remote.close();
const error = await navigationPromise;
expect(error.message).toBe('Navigation failed because browser has disconnected!');
expect(error.message).toContain('Navigation failed because browser has disconnected!');
await browserServer.close();
});
it('should reject waitForSelector when browser closes', async({server}) => {

View File

@ -355,14 +355,13 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
expect(request2.headers['referer']).toBe(undefined);
expect(page.url()).toBe(server.PREFIX + '/grid.html');
});
it.skip(FFOX)('should fail when canceled by another navigation', async({page, server}) => {
server.setRoute('/one-style.css', (req, res) => {});
// For some reason, Firefox issues load event with one outstanding request.
const failed = page.goto(server.PREFIX + '/one-style.html', { waitUntil: FFOX ? 'networkidle0' : 'load' }).catch(e => e);
await server.waitForRequest('/one-style.css');
it('should fail when canceled by another navigation', async({page, server}) => {
server.setRoute('/one-style.html', (req, res) => {});
const failed = page.goto(server.PREFIX + '/one-style.html').catch(e => e);
await server.waitForRequest('/one-style.html');
await page.goto(server.PREFIX + '/empty.html');
const error = await failed;
expect(error.message).toBe('Navigation to ' + server.PREFIX + '/one-style.html was canceled by another one');
expect(error.message).toBeTruthy();
});
describe('network idle', function() {
@ -502,7 +501,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
});
it.skip(FFOX)('should wait for networkidle0 in setContent with request from previous navigation', async({page, server}) => {
// TODO: in Firefox window.stop() does not cancel outstanding requests, and we also lack 'init' lifecycle,
// therefore we don't clear inglight requests at the right time.
// therefore we don't clear inflight requests at the right time.
await page.goto(server.EMPTY_PAGE);
server.setRoute('/foo.js', () => {});
await page.setContent(`<script>fetch('foo.js');</script>`);
@ -512,7 +511,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
});
it.skip(FFOX)('should wait for networkidle2 in setContent with request from previous navigation', async({page, server}) => {
// TODO: in Firefox window.stop() does not cancel outstanding requests, and we also lack 'init' lifecycle,
// therefore we don't clear inglight requests at the right time.
// therefore we don't clear inflight requests at the right time.
await page.goto(server.EMPTY_PAGE);
server.setRoute('/foo.js', () => {});
await page.setContent(`<script>fetch('foo.js');</script>`);
@ -660,7 +659,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
expect(forwardResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/second.html');
});
it.skip(FFOX)('should work when subframe issues window.stop()', async({page, server}) => {
it('should work when subframe issues window.stop()', async({page, server}) => {
server.setRoute('/frames/style.css', (req, res) => {});
const navigationPromise = page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = await new Promise(f => page.once('frameattached', f));
@ -726,30 +725,37 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
await waitPromise;
expect(resolved).toBe(true);
});
it('should work for cross-process navigations', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const waitPromise = page.waitForNavigation({waitUntil: []});
const url = server.CROSS_PROCESS_PREFIX + '/empty.html';
const gotoPromise = page.goto(url);
const response = await waitPromise;
expect(response.url()).toBe(url);
expect(page.url()).toBe(url);
expect(await page.evaluate('document.location.href')).toBe(url);
await gotoPromise;
});
});
describe('Page.waitForLoadState', () => {
it('should pick up ongoing navigation', async({page, server}) => {
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
await server.waitForRequest('/one-style.css');
await Promise.all([
server.waitForRequest('/one-style.css'),
page.goto(server.PREFIX + '/one-style.html', {waitUntil: []}),
]);
const waitPromise = page.waitForLoadState();
response.statusCode = 404;
response.end('Not found');
await waitPromise;
await navigationPromise;
});
it('should respect timeout', async({page, server}) => {
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
await server.waitForRequest('/one-style.css');
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: []});
const error = await page.waitForLoadState({ timeout: 1 }).catch(e => e);
expect(error.message).toBe('Navigation timeout of 1 ms exceeded');
response.statusCode = 404;
response.end('Not found');
await navigationPromise;
});
it('should resolve immediately if loaded', async({page, server}) => {
await page.goto(server.PREFIX + '/one-style.html');
@ -757,14 +763,9 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
});
it('should resolve immediately if load state matches', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
await server.waitForRequest('/one-style.css');
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: []});
await page.waitForLoadState({ waitUntil: 'domcontentloaded' });
response.statusCode = 404;
response.end('Not found');
await navigationPromise;
});
it.skip(FFOX)('should work with pages that have loaded before being connected to', async({page, context, server}) => {
await page.goto(server.EMPTY_PAGE);
@ -837,6 +838,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
await page.$eval('iframe', frame => frame.remove());
const error = await navigationPromise;
expect(error.message).toContain('frame was detached');
expect(error.stack).toContain('Frame.goto')
});
it('should return matching responses', async({page, server}) => {
// Disable cache: otherwise, chromium will cache similar requests.
@ -897,6 +899,24 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
});
});
describe('Frame.waitForLodState', function() {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];
const requestPromise = new Promise(resolve => page.route(server.PREFIX + '/one-style.css',resolve));
await frame.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
const request = await requestPromise;
let resolved = false;
const loadPromise = frame.waitForLoadState().then(() => resolved = true);
// give the promise a chance to resolve, even though it shouldn't
await page.evaluate('1');
expect(resolved).toBe(false);
request.continue();
await loadPromise;
});
});
describe('Page.reload', function() {
it('should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);

View File

@ -72,8 +72,7 @@ class TestServer {
/** @type {!Set<!NodeJS.Socket>} */
this._sockets = new Set();
/** @type {!Map<string, function(!IncomingMessage, !ServerResponse)>} */
/** @type {!Map<string, function(!http.IncomingMessage,http.ServerResponse)>} */
this._routes = new Map();
/** @type {!Map<string, !{username:string, password:string}>} */
this._auths = new Map();
@ -134,7 +133,7 @@ class TestServer {
/**
* @param {string} path
* @param {function(!IncomingMessage, !ServerResponse)} handler
* @param {function(!http.IncomingMessage,http.ServerResponse)} handler
*/
setRoute(path, handler) {
this._routes.set(path, handler);
@ -153,7 +152,7 @@ class TestServer {
/**
* @param {string} path
* @return {!Promise<!IncomingMessage>}
* @return {!Promise<!http.IncomingMessage>}
*/
waitForRequest(path) {
let promise = this._requestSubscribers.get(path);
@ -181,6 +180,10 @@ class TestServer {
this._requestSubscribers.clear();
}
/**
* @param {http.IncomingMessage} request
* @param {http.ServerResponse} response
*/
_onRequest(request, response) {
request.on('error', error => {
if (error.code === 'ECONNRESET')
@ -218,8 +221,8 @@ class TestServer {
}
/**
* @param {!IncomingMessage} request
* @param {!ServerResponse} response
* @param {!http.IncomingMessage} request
* @param {!http.ServerResponse} response
* @param {string} pathName
*/
serveFile(request, response, pathName) {