Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import * as debug from 'debug';
import * as types from './types';
import { TimeoutError } from './errors';

export const debugError = debug(`playwright:error`);
Expand Down Expand Up @@ -114,9 +115,13 @@ class Helper {
rejectCallback = reject;
});
const listener = Helper.addEventListener(emitter, eventName, event => {
if (!predicate(event))
return;
resolveCallback(event);
try {
if (!predicate(event))
return;
resolveCallback(event);
} catch (e) {
rejectCallback(e);
}
});
if (timeout) {
eventTimeout = setTimeout(() => {
Expand Down Expand Up @@ -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;
dgozman marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down
41 changes: 19 additions & 22 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
2 changes: 1 addition & 1 deletion test/network.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]);
Expand Down
106 changes: 98 additions & 8 deletions test/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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() {
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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() {
Expand Down