api(waitForLoadState): restore it (#1390)

This commit is contained in:
Pavel Feldman 2020-03-16 14:39:44 -07:00 committed by GitHub
parent 6731d37546
commit 64b175ce10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 35 deletions

View file

@ -686,6 +686,7 @@ page.removeListener('request', logRequest);
- [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) - [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args)
- [page.waitForEvent(event[, optionsOrPredicate])](#pagewaitforeventevent-optionsorpredicate) - [page.waitForEvent(event[, optionsOrPredicate])](#pagewaitforeventevent-optionsorpredicate)
- [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) - [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args)
- [page.waitForLoadState([options])](#pagewaitforloadstateoptions)
- [page.waitForNavigation([options])](#pagewaitfornavigationoptions) - [page.waitForNavigation([options])](#pagewaitfornavigationoptions)
- [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) - [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options)
- [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) - [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options)
@ -1703,6 +1704,26 @@ await page.waitForFunction(selector => !!document.querySelector(selector), {}, s
Shortcut for [page.mainFrame().waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args). Shortcut for [page.mainFrame().waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args).
#### page.waitForLoadState([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"> When to consider navigation succeeded, defaults to `load`. Events can be either:
- `'load'` - consider navigation to be finished when the `load` event is fired.
- `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
- `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms.
- returns: <[Promise]> Promise which resolves when the load state has been achieved.
This resolves when the page reaches a required load state, `load` by default. The navigation can be in progress when it is called.
If navigation is already at a required state, resolves immediately.
```js
await page.click('button'); // Click triggers navigation.
await page.waitForLoadState(); // The promise resolves after navigation has finished.
```
Shortcut for [page.mainFrame().waitForLoadState([options])](#framewaitforloadstateoptions).
#### page.waitForNavigation([options]) #### page.waitForNavigation([options])
- `options` <[Object]> Navigation parameters which might have the following properties: - `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
@ -1887,6 +1908,7 @@ An example of getting text from an iframe element:
- [frame.url()](#frameurl) - [frame.url()](#frameurl)
- [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) - [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args)
- [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) - [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args)
- [frame.waitForLoadState([options])](#framewaitforloadstateoptions)
- [frame.waitForNavigation([options])](#framewaitfornavigationoptions) - [frame.waitForNavigation([options])](#framewaitfornavigationoptions)
- [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) - [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
<!-- GEN:stop --> <!-- GEN:stop -->
@ -2374,6 +2396,24 @@ const selector = '.foo';
await page.waitForFunction(selector => !!document.querySelector(selector), {}, selector); await page.waitForFunction(selector => !!document.querySelector(selector), {}, selector);
``` ```
#### frame.waitForLoadState([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"> When to consider navigation succeeded, defaults to `load`. Events can be either:
- `'load'` - consider navigation to be finished when the `load` event is fired.
- `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
- `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms.
- returns: <[Promise]> Promise which resolves when the load state has been achieved.
This resolves when the page reaches a required load state, `load` by default. The navigation can be in progress when it is called.
If navigation is already at a required state, resolves immediately.
```js
await frame.click('button'); // Click triggers navigation.
await frame.waitForLoadState(); // The promise resolves after navigation has finished.
```
#### frame.waitForNavigation([options]) #### frame.waitForNavigation([options])
- `options` <[Object]> Navigation parameters which might have the following properties: - `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.

View file

@ -433,8 +433,8 @@ export class Frame {
return request ? request._finalRequest().response() : null; return request ? request._finalRequest().response() : null;
} }
async _waitForLoadState(options: types.NavigateOptions = {}): Promise<void> { async waitForLoadState(options: types.NavigateOptions = {}): Promise<void> {
const {timeout = this._page._timeoutSettings.navigationTimeout()} = options; const { timeout = this._page._timeoutSettings.navigationTimeout() } = options;
const disposer = new Disposer(); const disposer = new Disposer();
const error = await Promise.race([ const error = await Promise.race([
this._createFrameDestroyedPromise(), this._createFrameDestroyedPromise(),
@ -464,7 +464,7 @@ export class Frame {
let resolve: (error: {error?: Error, documentId: string}) => void; let resolve: (error: {error?: Error, documentId: string}) => void;
const promise = new Promise<{error?: Error, documentId: string}>(x => resolve = x); const promise = new Promise<{error?: Error, documentId: string}>(x => resolve = x);
const watch = (documentId: string, error?: Error) => { const watch = (documentId: string, error?: Error) => {
if (!error && !platform.urlMatches(this.url(), url)) if (!error && !helper.urlMatches(this.url(), url))
return; return;
resolve({error, documentId}); resolve({error, documentId});
}; };
@ -477,7 +477,7 @@ export class Frame {
let resolve: () => void; let resolve: () => void;
const promise = new Promise<void>(x => resolve = x); const promise = new Promise<void>(x => resolve = x);
const watch = () => { const watch = () => {
if (platform.urlMatches(this.url(), url)) if (helper.urlMatches(this.url(), url))
resolve(); resolve();
}; };
const dispose = () => this._sameDocumentNavigationWatchers.delete(watch); const dispose = () => this._sameDocumentNavigationWatchers.delete(watch);
@ -639,7 +639,7 @@ export class Frame {
this._page._frameManager._consoleMessageTags.set(tag, () => { this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below. // Clear lifecycle right after document.open() - see 'tag' below.
this._page._frameManager.clearFrameLifecycle(this); this._page._frameManager.clearFrameLifecycle(this);
this._waitForLoadState(options).then(resolve).catch(reject); this.waitForLoadState(options).then(resolve).catch(reject);
}); });
}); });
const contentPromise = context.evaluate((html, tag) => { const contentPromise = context.evaluate((html, tag) => {

View file

@ -279,6 +279,23 @@ class Helper {
static enclosingIntSize(size: types.Size): types.Size { static enclosingIntSize(size: types.Size): types.Size {
return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) }; return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) };
} }
static urlMatches(urlString: string, match: types.URLMatch | undefined): boolean {
if (match === undefined || match === '')
return true;
if (helper.isString(match))
match = helper.globToRegex(match);
if (helper.isRegExp(match))
return match.test(urlString);
if (typeof match === 'string' && match === urlString)
return true;
const url = new URL(urlString);
if (typeof match === 'string')
return url.pathname === match;
assert(typeof match === 'function', 'url parameter should be string, RegExp or function');
return match(url);
}
} }
export function assert(value: any, message?: string): asserts value { export function assert(value: any, message?: string): asserts value {

View file

@ -234,7 +234,7 @@ export class Page extends platform.EventEmitter {
return this.frames().find(f => { return this.frames().find(f => {
if (name) if (name)
return f.name() === name; return f.name() === name;
return platform.urlMatches(f.url(), url); return helper.urlMatches(f.url(), url);
}) || null; }) || null;
} }
@ -332,6 +332,10 @@ export class Page extends platform.EventEmitter {
return waitPromise; return waitPromise;
} }
async waitForLoadState(options?: types.NavigateOptions): Promise<void> {
return this.mainFrame().waitForLoadState(options);
}
async waitForNavigation(options?: types.WaitForNavigationOptions): Promise<network.Response | null> { async waitForNavigation(options?: types.WaitForNavigationOptions): Promise<network.Response | null> {
return this.mainFrame().waitForNavigation(options); return this.mainFrame().waitForNavigation(options);
} }
@ -347,7 +351,7 @@ export class Page extends platform.EventEmitter {
const { timeout = this._timeoutSettings.timeout() } = options; const { timeout = this._timeoutSettings.timeout() } = options;
return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => { return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => {
if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate))
return platform.urlMatches(request.url(), urlOrPredicate); return helper.urlMatches(request.url(), urlOrPredicate);
return urlOrPredicate(request); return urlOrPredicate(request);
}, timeout, this._disconnectedPromise); }, timeout, this._disconnectedPromise);
} }
@ -356,7 +360,7 @@ export class Page extends platform.EventEmitter {
const { timeout = this._timeoutSettings.timeout() } = options; const { timeout = this._timeoutSettings.timeout() } = options;
return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => { return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => {
if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate))
return platform.urlMatches(response.url(), urlOrPredicate); return helper.urlMatches(response.url(), urlOrPredicate);
return urlOrPredicate(response); return urlOrPredicate(response);
}, timeout, this._disconnectedPromise); }, timeout, this._disconnectedPromise);
} }
@ -423,13 +427,13 @@ export class Page extends platform.EventEmitter {
if (!route) if (!route)
return; return;
for (const { url, handler } of this._routes) { for (const { url, handler } of this._routes) {
if (platform.urlMatches(request.url(), url)) { if (helper.urlMatches(request.url(), url)) {
handler(route, request); handler(route, request);
return; return;
} }
} }
for (const { url, handler } of this._browserContext._routes) { for (const { url, handler } of this._browserContext._routes) {
if (platform.urlMatches(request.url(), url)) { if (helper.urlMatches(request.url(), url)) {
handler(route, request); handler(route, request);
return; return;
} }

View file

@ -28,7 +28,6 @@ import * as https from 'https';
import * as NodeWebSocket from 'ws'; import * as NodeWebSocket from 'ws';
import { assert, helper } from './helper'; import { assert, helper } from './helper';
import * as types from './types';
import { ConnectionTransport } from './transport'; import { ConnectionTransport } from './transport';
export const isNode = typeof process === 'object' && !!process && typeof process.versions === 'object' && !!process.versions && !!process.versions.node; export const isNode = typeof process === 'object' && !!process && typeof process.versions === 'object' && !!process.versions && !!process.versions.node;
@ -222,23 +221,6 @@ export function getMimeType(file: string): string {
return extensionToMime[extension] || 'application/octet-stream'; return extensionToMime[extension] || 'application/octet-stream';
} }
export function urlMatches(urlString: string, match: types.URLMatch | undefined): boolean {
if (match === undefined || match === '')
return true;
if (helper.isString(match))
match = helper.globToRegex(match);
if (helper.isRegExp(match))
return match.test(urlString);
if (typeof match === 'string' && match === urlString)
return true;
const url = new URL(urlString);
if (typeof match === 'string')
return url.pathname === match;
assert(typeof match === 'function', 'url parameter should be string, RegExp or function');
return match(url);
}
export function pngToJpeg(buffer: Buffer, quality?: number): Buffer { export function pngToJpeg(buffer: Buffer, quality?: number): Buffer {
assert(isNode, 'Converting from png to jpeg is only supported in Node.js'); assert(isNode, 'Converting from png to jpeg is only supported in Node.js');
return jpeg.encode(png.PNG.sync.read(buffer), quality).data; return jpeg.encode(png.PNG.sync.read(buffer), quality).data;

View file

@ -1011,7 +1011,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
}); });
}); });
describe('Page._waitForLoadState', () => { describe('Page.waitForLoadState', () => {
it('should pick up ongoing navigation', async({page, server}) => { it('should pick up ongoing navigation', async({page, server}) => {
let response = null; let response = null;
server.setRoute('/one-style.css', (req, res) => response = res); server.setRoute('/one-style.css', (req, res) => response = res);
@ -1019,7 +1019,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
server.waitForRequest('/one-style.css'), server.waitForRequest('/one-style.css'),
page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}), page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}),
]); ]);
const waitPromise = page.mainFrame()._waitForLoadState(); const waitPromise = page.waitForLoadState();
response.statusCode = 404; response.statusCode = 404;
response.end('Not found'); response.end('Not found');
await waitPromise; await waitPromise;
@ -1027,18 +1027,18 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
it('should respect timeout', async({page, server}) => { it('should respect timeout', async({page, server}) => {
server.setRoute('/one-style.css', (req, res) => response = res); server.setRoute('/one-style.css', (req, res) => response = res);
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}); await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
const error = await page.mainFrame()._waitForLoadState({ timeout: 1 }).catch(e => e); const error = await page.waitForLoadState({ timeout: 1 }).catch(e => e);
expect(error.message).toBe('Navigation timeout of 1 ms exceeded'); expect(error.message).toBe('Navigation timeout of 1 ms exceeded');
}); });
it('should resolve immediately if loaded', async({page, server}) => { it('should resolve immediately if loaded', async({page, server}) => {
await page.goto(server.PREFIX + '/one-style.html'); await page.goto(server.PREFIX + '/one-style.html');
await page.mainFrame()._waitForLoadState(); await page.waitForLoadState();
}); });
it('should resolve immediately if load state matches', async({page, server}) => { it('should resolve immediately if load state matches', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
server.setRoute('/one-style.css', (req, res) => response = res); server.setRoute('/one-style.css', (req, res) => response = res);
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}); await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
await page.mainFrame()._waitForLoadState({ waitUntil: 'domcontentloaded' }); await page.waitForLoadState({ waitUntil: 'domcontentloaded' });
}); });
it('should work with pages that have loaded before being connected to', async({page, context, server}) => { it('should work with pages that have loaded before being connected to', async({page, context, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -1047,7 +1047,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
page.evaluate(() => window._popup = window.open(document.location.href)), page.evaluate(() => window._popup = window.open(document.location.href)),
]); ]);
expect(popup.url()).toBe(server.EMPTY_PAGE); expect(popup.url()).toBe(server.EMPTY_PAGE);
await popup.mainFrame()._waitForLoadState(); await popup.waitForLoadState();
expect(popup.url()).toBe(server.EMPTY_PAGE); expect(popup.url()).toBe(server.EMPTY_PAGE);
}); });
}); });
@ -1171,7 +1171,7 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
await frame.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}); await frame.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
const request = await requestPromise; const request = await requestPromise;
let resolved = false; let resolved = false;
const loadPromise = frame._waitForLoadState().then(() => resolved = true); const loadPromise = frame.waitForLoadState().then(() => resolved = true);
// give the promise a chance to resolve, even though it shouldn't // give the promise a chance to resolve, even though it shouldn't
await page.evaluate('1'); await page.evaluate('1');
expect(resolved).toBe(false); expect(resolved).toBe(false);