feature(waitFor): use URLMatch to match request/response, waitForEvent for generic (#278)

This commit is contained in:
Dmitry Gozman 2019-12-17 14:00:39 -08:00 committed by GitHub
parent 0dafabb05e
commit 75ae9bfeee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 230 additions and 34 deletions

View File

@ -16,6 +16,7 @@
*/ */
import * as debug from 'debug'; import * as debug from 'debug';
import * as types from './types';
import { TimeoutError } from './errors'; import { TimeoutError } from './errors';
export const debugError = debug(`playwright:error`); export const debugError = debug(`playwright:error`);
@ -114,9 +115,13 @@ class Helper {
rejectCallback = reject; rejectCallback = reject;
}); });
const listener = Helper.addEventListener(emitter, eventName, event => { const listener = Helper.addEventListener(emitter, eventName, event => {
if (!predicate(event)) try {
return; if (!predicate(event))
resolveCallback(event); return;
resolveCallback(event);
} catch (e) {
rejectCallback(e);
}
}); });
if (timeout) { if (timeout) {
eventTimeout = setTimeout(() => { eventTimeout = setTimeout(() => {
@ -153,6 +158,93 @@ class Helper {
clearTimeout(timeoutTimer); clearTimeout(timeoutTimer);
} }
} }
static stringMatches(s: string, match: string | RegExp, name: string): boolean {
if (helper.isString(match))
return s === match;
if (match instanceof RegExp)
return match.test(s);
throw new Error(`url match field "${name}" must be a string or a RegExp, got ${typeof match}`);
}
static searchParamsMatch(params: URLSearchParams, match: types.SearchParamsMatch, strict: boolean, name: string): boolean {
if (typeof match !== 'object' || match === null)
throw new Error(`url match field "${name}" must be an object, got ${typeof match}`);
const keys = new Set((params as any).keys()) as Set<string>;
if (strict && keys.size !== Object.keys(match).length)
return false;
for (const key of keys) {
let expected = [];
if (key in match) {
let keyMatch = match[key];
if (!Array.isArray(keyMatch))
keyMatch = [keyMatch];
expected = keyMatch;
} else if (!strict) {
continue;
}
const values = params.getAll(key);
if (strict && values.length !== expected.length)
return false;
for (const v of values) {
let found = false;
for (const e of expected) {
if (helper.stringMatches(v, e, name + '.' + key)) {
found = true;
break;
}
}
if (!found)
return false;
}
}
return true;
}
static urlMatches(urlString: string, match: types.URLMatch): boolean {
let url;
try {
url = new URL(urlString);
} catch (e) {
return urlString === match.url &&
match.hash === undefined &&
match.host === undefined &&
match.hostname === undefined &&
match.origin === undefined &&
match.password === undefined &&
match.pathname === undefined &&
match.port === undefined &&
match.protocol === undefined &&
match.search === undefined &&
match.searchParams === undefined &&
match.username === undefined;
}
if (match.url !== undefined && !helper.stringMatches(urlString, match.url, 'url'))
return false;
if (match.hash !== undefined && !helper.stringMatches(url.hash, match.hash, 'hash'))
return false;
if (match.host !== undefined && !helper.stringMatches(url.host, match.host, 'host'))
return false;
if (match.hostname !== undefined && !helper.stringMatches(url.hostname, match.hostname, 'hostname'))
return false;
if (match.origin !== undefined && !helper.stringMatches(url.origin, match.origin, 'origin'))
return false;
if (match.password !== undefined && !helper.stringMatches(url.password, match.password, 'password'))
return false;
if (match.pathname !== undefined && !helper.stringMatches(url.pathname, match.pathname, 'pathname'))
return false;
if (match.port !== undefined && !helper.stringMatches(url.port, match.port, 'port'))
return false;
if (match.protocol !== undefined && !helper.stringMatches(url.protocol, match.protocol, 'protocol'))
return false;
if (match.search !== undefined && !helper.stringMatches(url.search, match.search, 'search'))
return false;
if (match.username !== undefined && !helper.stringMatches(url.username, match.username, 'username'))
return false;
if (match.searchParams !== undefined && !helper.searchParamsMatch(url.searchParams, match.searchParams, !!match.strictSearchParams, 'searchParams'))
return false;
return true;
}
} }
export function assert(value: any, message?: string) { export function assert(value: any, message?: string) {

View File

@ -151,10 +151,8 @@ export class Page extends EventEmitter {
this.emit(Events.Page.FileChooser, fileChooser); this.emit(Events.Page.FileChooser, fileChooser);
} }
async waitForFileChooser(options: { timeout?: number; } = {}): Promise<FileChooser> { async waitForFileChooser(options: types.TimeoutOptions = {}): Promise<FileChooser> {
const { const { timeout = this._timeoutSettings.timeout() } = options;
timeout = this._timeoutSettings.timeout(),
} = options;
let callback; let callback;
const promise = new Promise<FileChooser>(x => callback = x); const promise = new Promise<FileChooser>(x => callback = x);
this._fileChooserInterceptors.add(callback); this._fileChooserInterceptors.add(callback);
@ -333,29 +331,28 @@ export class Page extends EventEmitter {
return this.mainFrame().waitForNavigation(options); return this.mainFrame().waitForNavigation(options);
} }
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> { async waitForEvent(event: string, options: Function | (types.TimeoutOptions & { predicate?: Function }) = {}): Promise<any> {
const { if (typeof options === 'function')
timeout = this._timeoutSettings.timeout(), options = { predicate: options };
} = options; const { timeout = this._timeoutSettings.timeout(), predicate = () => true } = options;
return helper.waitForEvent(this, event, (...args: any[]) => !!predicate(...args), timeout, this._disconnectedPromise);
}
async waitForRequest(options: string | (types.URLMatch & types.TimeoutOptions) = {}): Promise<Request> {
if (helper.isString(options))
options = { url: 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)) return helper.urlMatches(request.url(), options as types.URLMatch);
return (urlOrPredicate === request.url());
if (typeof urlOrPredicate === 'function')
return !!(urlOrPredicate(request));
return false;
}, timeout, this._disconnectedPromise); }, timeout, this._disconnectedPromise);
} }
async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<network.Response> { async waitForResponse(options: string | (types.URLMatch & types.TimeoutOptions) = {}): Promise<Request> {
const { if (helper.isString(options))
timeout = this._timeoutSettings.timeout(), options = { url: options };
} = 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)) return helper.urlMatches(response.url(), options as types.URLMatch);
return (urlOrPredicate === response.url());
if (typeof urlOrPredicate === 'function')
return !!(urlOrPredicate(response));
return false;
}, timeout, this._disconnectedPromise); }, timeout, this._disconnectedPromise);
} }

View File

@ -72,3 +72,20 @@ export type Viewport = {
isLandscape?: boolean; isLandscape?: boolean;
hasTouch?: boolean; hasTouch?: boolean;
}; };
export type SearchParamsMatch = { [key: string]: string | RegExp | (string | RegExp)[] };
export type URLMatch = {
url?: string | RegExp,
hash?: string | RegExp,
host?: string | RegExp,
hostname?: string | RegExp,
origin?: string | RegExp,
password?: string | RegExp,
pathname?: string | RegExp,
port?: string | RegExp,
protocol?: string | RegExp,
search?: string | RegExp,
strictSearchParams?: boolean,
searchParams?: SearchParamsMatch,
username?: string | RegExp,
};

View File

@ -152,7 +152,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get')); page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get'));
// send request and wait for server response // send request and wait for server response
const [pageResponse] = await Promise.all([ const [pageResponse] = await Promise.all([
page.waitForResponse(r => !utils.isFavicon(r.request())), page.waitForEvent('response', { predicate: r => !utils.isFavicon(r.request()) }),
page.evaluate(() => fetch('./get', { method: 'GET'})), page.evaluate(() => fetch('./get', { method: 'GET'})),
server.waitForRequest('/get'), server.waitForRequest('/get'),
]); ]);

View File

@ -299,7 +299,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
it('should work with predicate', async({page, server}) => { it('should work with predicate', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([ const [request] = await Promise.all([
page.waitForRequest(request => request.url() === server.PREFIX + '/digits/2.png'), page.waitForEvent('request', request => request.url() === server.PREFIX + '/digits/2.png'),
page.evaluate(() => { page.evaluate(() => {
fetch('/digits/1.png'); fetch('/digits/1.png');
fetch('/digits/2.png'); fetch('/digits/2.png');
@ -310,19 +310,19 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
}); });
it('should respect timeout', async({page, server}) => { it('should respect timeout', async({page, server}) => {
let error = null; let error = null;
await page.waitForRequest(() => false, {timeout: 1}).catch(e => error = e); await page.waitForEvent('request', { predicate: () => false, timeout: 1 }).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
it('should respect default timeout', async({page, server}) => { it('should respect default timeout', async({page, server}) => {
let error = null; let error = null;
page.setDefaultTimeout(1); page.setDefaultTimeout(1);
await page.waitForRequest(() => false).catch(e => error = e); await page.waitForEvent('request', () => false).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
it('should work with no timeout', async({page, server}) => { it('should work with no timeout', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([ const [request] = await Promise.all([
page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}), page.waitForRequest({url: server.PREFIX + '/digits/2.png', timeout: 0}),
page.evaluate(() => setTimeout(() => { page.evaluate(() => setTimeout(() => {
fetch('/digits/1.png'); fetch('/digits/1.png');
fetch('/digits/2.png'); fetch('/digits/2.png');
@ -331,6 +331,84 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
]); ]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
}); });
it('should work with url match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest({ url: /digits\/\d\.png/ }),
page.evaluate(() => {
fetch('/digits/1.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/1.png');
});
it('should work with pathname match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest({ pathname: '/digits/2.png' }),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should work with multiple matches', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest({ pathname: '/digits/2.png', url: /\d\.png/, port: String(server.PORT) }),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should work with strict search params match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest({ searchParams: { 'foo': [/^baz$/, 'bar'], 'bar': 'foo' }, strictSearchParams: true }),
page.evaluate(() => {
fetch('/digits/2.png?foo=bar&foo=baz&bar=foo&key=value');
fetch('/digits/1.png?foo=bar&bar=foo');
fetch('/digits/4.png?foo=bar&bar=foo&foo=baz');
fetch('/digits/3.png?bar=foo');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/4.png?foo=bar&bar=foo&foo=baz');
});
it('should work with relaxed search params match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest({ searchParams: { 'foo': ['bar', /^baz$/], 'bar': 'foo' } }),
page.evaluate(() => {
fetch('/digits/1.png?key=value&foo=something');
fetch('/digits/2.png?foo=baz');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png?foo=baz');
});
it('should throw for incorrect match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [error] = await Promise.all([
page.waitForRequest({ url: null }).catch(e => e),
page.evaluate(() => {
fetch('/digits/1.png');
})
]);
expect(error.message).toBe('url match field "url" must be a string or a RegExp, got object');
});
it('should throw for incorrect searchParams match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [error] = await Promise.all([
page.waitForRequest({ searchParams: { 'foo': 123 } }).catch(e => e),
page.evaluate(() => {
fetch('/digits/1.png?foo=bar');
})
]);
expect(error.message).toBe('url match field "searchParams.foo" must be a string or a RegExp, got number');
});
}); });
describe('Page.waitForResponse', function() { describe('Page.waitForResponse', function() {
@ -348,19 +426,19 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
}); });
it('should respect timeout', async({page, server}) => { it('should respect timeout', async({page, server}) => {
let error = null; let error = null;
await page.waitForResponse(() => false, {timeout: 1}).catch(e => error = e); await page.waitForEvent('response', { predicate: () => false, timeout: 1 }).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
it('should respect default timeout', async({page, server}) => { it('should respect default timeout', async({page, server}) => {
let error = null; let error = null;
page.setDefaultTimeout(1); page.setDefaultTimeout(1);
await page.waitForResponse(() => false).catch(e => error = e); await page.waitForEvent('response', () => false).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
it('should work with predicate', async({page, server}) => { it('should work with predicate', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([ const [response] = await Promise.all([
page.waitForResponse(response => response.url() === server.PREFIX + '/digits/2.png'), page.waitForEvent('response', response => response.url() === server.PREFIX + '/digits/2.png'),
page.evaluate(() => { page.evaluate(() => {
fetch('/digits/1.png'); fetch('/digits/1.png');
fetch('/digits/2.png'); fetch('/digits/2.png');
@ -372,7 +450,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
it('should work with no timeout', async({page, server}) => { it('should work with no timeout', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([ const [response] = await Promise.all([
page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}), page.waitForResponse({ url: server.PREFIX + '/digits/2.png', timeout: 0 }),
page.evaluate(() => setTimeout(() => { page.evaluate(() => setTimeout(() => {
fetch('/digits/1.png'); fetch('/digits/1.png');
fetch('/digits/2.png'); fetch('/digits/2.png');
@ -381,6 +459,18 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
]); ]);
expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
}); });
it('should work with multiple matches', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse({ pathname: '/digits/2.png', url: /\d\.png/, port: String(server.PORT) }),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
});
}); });
describe('Page.exposeFunction', function() { describe('Page.exposeFunction', function() {