mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 13:45:36 +03:00
feature(waitFor): use URLMatch to match request/response, waitForEvent for generic (#278)
This commit is contained in:
parent
0dafabb05e
commit
75ae9bfeee
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as debug from 'debug';
|
||||
import * as types from './types';
|
||||
import { TimeoutError } from './errors';
|
||||
|
||||
export const debugError = debug(`playwright:error`);
|
||||
@ -114,9 +115,13 @@ class Helper {
|
||||
rejectCallback = reject;
|
||||
});
|
||||
const listener = Helper.addEventListener(emitter, eventName, event => {
|
||||
try {
|
||||
if (!predicate(event))
|
||||
return;
|
||||
resolveCallback(event);
|
||||
} catch (e) {
|
||||
rejectCallback(e);
|
||||
}
|
||||
});
|
||||
if (timeout) {
|
||||
eventTimeout = setTimeout(() => {
|
||||
@ -153,6 +158,93 @@ class Helper {
|
||||
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) {
|
||||
|
41
src/page.ts
41
src/page.ts
@ -151,10 +151,8 @@ export class Page extends EventEmitter {
|
||||
this.emit(Events.Page.FileChooser, fileChooser);
|
||||
}
|
||||
|
||||
async waitForFileChooser(options: { timeout?: number; } = {}): Promise<FileChooser> {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
async waitForFileChooser(options: types.TimeoutOptions = {}): Promise<FileChooser> {
|
||||
const { timeout = this._timeoutSettings.timeout() } = options;
|
||||
let callback;
|
||||
const promise = new Promise<FileChooser>(x => callback = x);
|
||||
this._fileChooserInterceptors.add(callback);
|
||||
@ -333,29 +331,28 @@ export class Page extends EventEmitter {
|
||||
return this.mainFrame().waitForNavigation(options);
|
||||
}
|
||||
|
||||
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
async waitForEvent(event: string, options: Function | (types.TimeoutOptions & { predicate?: Function }) = {}): Promise<any> {
|
||||
if (typeof options === 'function')
|
||||
options = { predicate: 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) => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === request.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(request));
|
||||
return false;
|
||||
return helper.urlMatches(request.url(), options as types.URLMatch);
|
||||
}, timeout, this._disconnectedPromise);
|
||||
}
|
||||
|
||||
async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<network.Response> {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
async waitForResponse(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.Response, (response: network.Response) => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === response.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(response));
|
||||
return false;
|
||||
return helper.urlMatches(response.url(), options as types.URLMatch);
|
||||
}, timeout, this._disconnectedPromise);
|
||||
}
|
||||
|
||||
|
17
src/types.ts
17
src/types.ts
@ -72,3 +72,20 @@ export type Viewport = {
|
||||
isLandscape?: 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,
|
||||
};
|
||||
|
@ -152,7 +152,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
|
||||
page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get'));
|
||||
// send request and wait for server response
|
||||
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'})),
|
||||
server.waitForRequest('/get'),
|
||||
]);
|
||||
|
@ -299,7 +299,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
|
||||
it('should work with predicate', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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(() => {
|
||||
fetch('/digits/1.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}) => {
|
||||
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);
|
||||
});
|
||||
it('should respect default timeout', async({page, server}) => {
|
||||
let error = null;
|
||||
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);
|
||||
});
|
||||
it('should work with no timeout', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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(() => {
|
||||
fetch('/digits/1.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');
|
||||
});
|
||||
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() {
|
||||
@ -348,19 +426,19 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
|
||||
});
|
||||
it('should respect timeout', async({page, server}) => {
|
||||
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);
|
||||
});
|
||||
it('should respect default timeout', async({page, server}) => {
|
||||
let error = null;
|
||||
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);
|
||||
});
|
||||
it('should work with predicate', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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(() => {
|
||||
fetch('/digits/1.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}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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(() => {
|
||||
fetch('/digits/1.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');
|
||||
});
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user