mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(test-runner): introduce actionTimeout and navigationTimeout (#7919)
This commit is contained in:
parent
34c0c342fa
commit
4163cec93b
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
? () =>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
14
types/test.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
@ -308,6 +308,8 @@ export interface PlaywrightTestOptions {
|
||||
viewport: ViewportSize | null | undefined;
|
||||
baseURL: string | undefined;
|
||||
contextOptions: BrowserContextOptions;
|
||||
actionTimeout: number | undefined;
|
||||
navigationTimeout: number | undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user