feat(test-runner): introduce actionTimeout and navigationTimeout (#7919)

This commit is contained in:
Pavel Feldman 2021-07-29 21:03:50 -07:00 committed by GitHub
parent 34c0c342fa
commit 4163cec93b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 25 deletions

View File

@ -115,6 +115,13 @@ const config: PlaywrightTestConfig = {
export default config;
```
## property: Fixtures.actionTimeout
- type: <[int]>
Timeout for each action and expect in milliseconds. Defaults to 0 (no timeout).
This is a default timeout for all Playwright actions, same as configured via [`method: Page.setDefaultTimeout`].
## property: Fixtures.bypassCSP = %%-context-option-bypasscsp-%%
## property: Fixtures.channel = %%-browser-option-channel-%%
@ -220,6 +227,13 @@ Options used to launch the browser, as passed to [`method: BrowserType.launch`].
## property: Fixtures.locale = %%-context-option-locale-%%
## property: Fixtures.navigationTimeout
- type: <[int]>
Timeout for each navigation action in milliseconds. Defaults to 0 (no timeout).
This is a default navigation timeout, same as configured via [`method: Page.setDefaultNavigationTimeout`].
## property: Fixtures.offline = %%-context-option-offline-%%
## property: Fixtures.page

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import expectLibrary from 'expect';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@ -74,12 +75,42 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
timezoneId: undefined,
userAgent: undefined,
viewport: undefined,
actionTimeout: undefined,
navigationTimeout: undefined,
baseURL: async ({ }, use) => {
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
},
contextOptions: {},
createContext: async ({ browser, screenshot, trace, video, acceptDownloads, bypassCSP, colorScheme, deviceScaleFactor, extraHTTPHeaders, hasTouch, geolocation, httpCredentials, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, proxy, storageState, viewport, timezoneId, userAgent, baseURL, contextOptions }, use, testInfo) => {
createContext: async ({
browser,
screenshot,
trace,
video,
acceptDownloads,
bypassCSP,
colorScheme,
deviceScaleFactor,
extraHTTPHeaders,
hasTouch,
geolocation,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
viewport,
timezoneId,
userAgent,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout
}, use, testInfo) => {
testInfo.snapshotSuffix = process.platform;
if (process.env.PWDEBUG)
testInfo.setTimeout(0);
@ -153,7 +184,9 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
...additionalOptions,
};
const context = await browser.newContext(combinedOptions);
context.setDefaultTimeout(0);
context.setDefaultTimeout(actionTimeout || 0);
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
expectLibrary.setState({ playwrightActionTimeout: actionTimeout } as any);
context.on('page', page => allPages.push(page));
if (captureTrace) {

View File

@ -20,7 +20,7 @@ import {
} from 'jest-matcher-utils';
import { currentTestInfo } from '../globals';
import type { Expect } from '../types';
import { expectType, monotonicTime, pollUntilDeadline } from '../util';
import { expectType, pollUntilDeadline } from '../util';
export async function toBeTruthy<T>(
this: ReturnType<Expect['getState']>,
@ -42,16 +42,13 @@ export async function toBeTruthy<T>(
let received: T;
let pass = false;
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
const deadline = timeout ? monotonicTime() + timeout : 0;
// TODO: interrupt on timeout for nice message.
await pollUntilDeadline(async () => {
const remainingTime = deadline ? deadline - monotonicTime() : 0;
await pollUntilDeadline(this, async remainingTime => {
received = await query(remainingTime);
pass = !!received;
return pass === !matcherOptions.isNot;
}, deadline, 100);
}, options.timeout, 100);
const message = () => {
return matcherHint(matcherName, undefined, '', matcherOptions);

View File

@ -27,7 +27,7 @@ import {
} from 'jest-matcher-utils';
import { currentTestInfo } from '../globals';
import type { Expect } from '../types';
import { expectType, monotonicTime, pollUntilDeadline } from '../util';
import { expectType, pollUntilDeadline } from '../util';
// Omit colon and one or more spaces, so can call getLabelPrinter.
const EXPECTED_LABEL = 'Expected';
@ -58,16 +58,13 @@ export async function toEqual<T>(
let received: T | undefined = undefined;
let pass = false;
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
const deadline = timeout ? monotonicTime() + timeout : 0;
// TODO: interrupt on timeout for nice message.
await pollUntilDeadline(async () => {
const remainingTime = deadline ? deadline - monotonicTime() : 0;
await pollUntilDeadline(this, async remainingTime => {
received = await query(remainingTime);
pass = equals(received, expected, [iterableEquality]);
return pass === !matcherOptions.isNot;
}, deadline, 100);
}, options.timeout, 100);
const message = pass
? () =>

View File

@ -30,7 +30,7 @@ import {
} from 'jest-matcher-utils';
import { currentTestInfo } from '../globals';
import type { Expect } from '../types';
import { expectType, monotonicTime, pollUntilDeadline } from '../util';
import { expectType, pollUntilDeadline } from '../util';
export async function toMatchText(
this: ReturnType<Expect['getState']>,
@ -68,12 +68,9 @@ export async function toMatchText(
let received: string;
let pass = false;
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
const deadline = timeout ? monotonicTime() + timeout : 0;
// TODO: interrupt on timeout for nice message.
await pollUntilDeadline(async () => {
const remainingTime = deadline ? deadline - monotonicTime() : 0;
await pollUntilDeadline(this, async remainingTime => {
received = await query(remainingTime);
if (options.matchSubstring)
pass = received.includes(expected as string);
@ -83,7 +80,7 @@ export async function toMatchText(
pass = expected.test(received);
return pass === !matcherOptions.isNot;
}, deadline, 100);
}, options.timeout, 100);
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
const message = pass

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { Expect } from './types';
import util from 'util';
import path from 'path';
import type { TestError, Location } from './types';
@ -70,21 +71,25 @@ export async function raceAgainstDeadline<T>(promise: Promise<T>, deadline: numb
return (new DeadlineRunner(promise, deadline)).result;
}
export async function pollUntilDeadline(func: () => Promise<boolean>, deadline: number, delay: number): Promise<void> {
export async function pollUntilDeadline(state: ReturnType<Expect['getState']>, func: (remainingTime: number) => Promise<boolean>, pollTime: number | undefined, pollInterval: number): Promise<void> {
const playwrightActionTimeout = (state as any).playwrightActionTimeout;
pollTime = pollTime === 0 ? 0 : pollTime || playwrightActionTimeout;
const deadline = pollTime ? monotonicTime() + pollTime : 0;
while (true) {
const timeUntilDeadline = deadline ? deadline - monotonicTime() : Number.MAX_VALUE;
if (timeUntilDeadline <= 0)
const remainingTime = deadline ? deadline - monotonicTime() : 1000 * 3600 * 24;
if (remainingTime <= 0)
break;
try {
if (await func())
if (await func(remainingTime))
return;
} catch (e) {
if (e instanceof errors.TimeoutError)
return;
throw e;
}
await new Promise(f => setTimeout(f, delay));
await new Promise(f => setTimeout(f, pollInterval));
}
}

View File

@ -158,3 +158,24 @@ test('should support toHaveURL', async ({ runInlineTest }) => {
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});
test('should support respect actionTimeout', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `module.exports = { use: { actionTimeout: 1000 } }`,
'a.test.ts': `
const { test } = pwt;
test('timeout', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
await Promise.all([
expect(page).toHaveURL('data:text/html,<div>B</div>'),
new Promise(f => setTimeout(f, 2000)).then(() => expect(true).toBe(false))
]);
});
`,
}, { workers: 1 });
const output = stripAscii(result.output);
expect(output).toContain('expect(received).toHaveURL(expected)');
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});

14
types/test.d.ts vendored
View File

@ -2490,6 +2490,20 @@ export interface PlaywrightTestOptions {
* like [fixtures.viewport](https://playwright.dev/docs/api/class-fixtures#fixtures-viewport) take priority over this.
*/
contextOptions: BrowserContextOptions;
/**
* Timeout for each action and expect in milliseconds. Defaults to 0 (no timeout).
*
* This is a default timeout for all Playwright actions, same as configured via
* [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout).
*/
actionTimeout: number | undefined;
/**
* Timeout for each navigation action in milliseconds. Defaults to 0 (no timeout).
*
* This is a default navigation timeout, same as configured via
* [page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout).
*/
navigationTimeout: number | undefined;
}

View File

@ -308,6 +308,8 @@ export interface PlaywrightTestOptions {
viewport: ViewportSize | null | undefined;
baseURL: string | undefined;
contextOptions: BrowserContextOptions;
actionTimeout: number | undefined;
navigationTimeout: number | undefined;
}