frame.waitForLoadState test, convert waiters to methods

This commit is contained in:
Joel Einbinder 2020-02-07 15:41:25 -08:00
parent 6ed0a8b427
commit a4c806a68e
2 changed files with 134 additions and 131 deletions

View file

@ -355,6 +355,8 @@ 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;
@ -362,6 +364,8 @@ export class Frame {
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);
@ -383,9 +387,9 @@ export class Frame {
const disposer = new Disposer();
const timeoutPromise = disposer.add(createTimeoutPromise(timeout));
const frameDestroyedPromise = disposer.add(createFrameDestroyedPromise(this));
const sameDocumentPromise = disposer.add(waitForSameDocumentNavigation(this));
const requestWatcher = disposer.add(trackDocumentRequests(this));
const frameDestroyedPromise = this._createFrameDestroyedPromise();
const sameDocumentPromise = disposer.add(this._waitForSameDocumentNavigation());
const requestWatcher = disposer.add(this._trackDocumentRequests());
let navigateResult: GotoResult;
const navigate = async () => {
try {
@ -403,13 +407,13 @@ export class Frame {
const promises: Promise<Error|void>[] = [timeoutPromise, frameDestroyedPromise];
if (navigateResult!.newDocumentId)
promises.push(disposer.add(waitForSpecificDocument(this, 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(waitForLifecycle(this, options.waitUntil));
const waitForLifecyclePromise = disposer.add(this._waitForLifecycle(options.waitUntil));
throwIfError(await Promise.race([timeoutPromise, frameDestroyedPromise, waitForLifecyclePromise]));
disposer.dispose();
@ -429,28 +433,28 @@ export class Frame {
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
const disposer = new Disposer();
const requestWatcher = disposer.add(trackDocumentRequests(this));
const requestWatcher = disposer.add(this._trackDocumentRequests());
const {timeout = this._page._timeoutSettings.navigationTimeout()} = options;
const failurePromise = Promise.race([
this._createFrameDestroyedPromise(),
disposer.add(createTimeoutPromise(timeout)),
disposer.add(createFrameDestroyedPromise(this)),
]);
let documentId: string|null = null;
let error: void|Error = await Promise.race([
failurePromise,
disposer.add(waitForNewDocument(this, options.url)).then(result => {
disposer.add(this._waitForNewDocument(options.url)).then(result => {
if (result.error)
return result.error;
documentId = result.documentId;
}),
disposer.add(waitForSameDocumentNavigation(this, options.url)),
disposer.add(this._waitForSameDocumentNavigation(options.url)),
]);
const request = requestWatcher.get(documentId!);
if (!error) {
error = await Promise.race([
failurePromise,
disposer.add(waitForLifecycle(this, options.waitUntil)),
disposer.add(this._waitForLifecycle(options.waitUntil)),
]);
}
disposer.dispose();
@ -463,14 +467,107 @@ export class Frame {
const {timeout = this._page._timeoutSettings.navigationTimeout()} = options;
const disposer = new Disposer();
const error = await Promise.race([
this._createFrameDestroyedPromise(),
disposer.add(createTimeoutPromise(timeout)),
disposer.add(waitForLifecycle(this, options.waitUntil)),
disposer.add(this._waitForLifecycle(options.waitUntil)),
]);
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);
}
@ -854,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.'));
@ -999,26 +1097,6 @@ class Disposer {
}
}
function createFrameDestroyedPromise(frame: Frame): Disposable<Promise<Error>> {
let detachedCallback: (error: Error) => void;
const detachedPromise = new Promise<Error>(x => detachedCallback = x);
const listener = (detachedFrame: Frame) => {
if (detachedFrame !== frame)
return;
detachedCallback(new Error('Navigating frame was detached!'));
dispose();
};
const dispose = () => {
frame._page.removeListener(Events.Page.FrameDetached, listener);
};
frame._page.addListener(Events.Page.FrameDetached, listener);
const promise = Promise.race([
frame._page._disconnectedPromise.then(() => new Error('Navigation failed because browser has disconnected!')),
detachedPromise,
]);
return {value: promise, dispose};
}
function createTimeoutPromise(timeout: number): Disposable<Promise<TimeoutError>> {
if (!timeout)
return { value: new Promise(() => {}), dispose: () => void 0 };
@ -1036,91 +1114,6 @@ function createTimeoutPromise(timeout: number): Disposable<Promise<TimeoutError>
};
}
function waitForSpecificDocument(frame: Frame, 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 = () => frame._documentWatchers.delete(watch);
frame._documentWatchers.add(watch);
return {value: promise, dispose};
}
function waitForNewDocument(frame: Frame, 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(frame.url(), url))
return;
resolve({error, documentId});
};
const dispose = () => frame._documentWatchers.delete(watch);
frame._documentWatchers.add(watch);
return {value: promise, dispose};
}
function waitForSameDocumentNavigation(frame: Frame, url?: types.URLMatch): Disposable<Promise<void>> {
let resolve: () => void;
const promise = new Promise<void>(x => resolve = x);
const watch = () => {
if (platform.urlMatches(frame.url(), url))
resolve();
};
const dispose = () => frame._sameDocumentNavigationWatchers.delete(watch);
frame._sameDocumentNavigationWatchers.add(watch);
return {value: promise, dispose};
}
function waitForLifecycle(frame: Frame, 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 promise = new Promise<void>(x => resolve = x);
const dispose = () => frame._page._frameManager._lifecycleWatchers.delete(checkLifecycleComplete);
frame._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;
}
function checkLifecycleComplete() {
if (!checkLifecycleRecursively(frame))
return;
resolve();
}
}
function trackDocumentRequests(frame: Frame): Disposable<Map<string, network.Request>> {
const requestMap = new Map<string, network.Request>();
const dispose = () => {
frame._requestWatchers.delete(onRequest);
};
const onRequest = (request: network.Request) => {
if (!request._documentId || request.redirectChain().length)
return;
requestMap.set(request._documentId, request);
};
frame._requestWatchers.add(onRequest);
return {dispose, value: requestMap};
}
function selectorToString(selector: string, visibility: types.Visibility): string {
let label;
switch (visibility) {

View file

@ -739,24 +739,20 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
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');
@ -764,14 +760,9 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
});
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);
@ -844,6 +835,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
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.
@ -904,6 +896,24 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
});
});
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);