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;
|
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.bypassCSP = %%-context-option-bypasscsp-%%
|
||||||
|
|
||||||
## property: Fixtures.channel = %%-browser-option-channel-%%
|
## 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.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.offline = %%-context-option-offline-%%
|
||||||
|
|
||||||
## property: Fixtures.page
|
## property: Fixtures.page
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import expectLibrary from 'expect';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
|
@ -74,12 +75,42 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||||
timezoneId: undefined,
|
timezoneId: undefined,
|
||||||
userAgent: undefined,
|
userAgent: undefined,
|
||||||
viewport: undefined,
|
viewport: undefined,
|
||||||
|
actionTimeout: undefined,
|
||||||
|
navigationTimeout: undefined,
|
||||||
baseURL: async ({ }, use) => {
|
baseURL: async ({ }, use) => {
|
||||||
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
|
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
|
||||||
},
|
},
|
||||||
contextOptions: {},
|
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;
|
testInfo.snapshotSuffix = process.platform;
|
||||||
if (process.env.PWDEBUG)
|
if (process.env.PWDEBUG)
|
||||||
testInfo.setTimeout(0);
|
testInfo.setTimeout(0);
|
||||||
|
|
@ -153,7 +184,9 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||||
...additionalOptions,
|
...additionalOptions,
|
||||||
};
|
};
|
||||||
const context = await browser.newContext(combinedOptions);
|
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));
|
context.on('page', page => allPages.push(page));
|
||||||
|
|
||||||
if (captureTrace) {
|
if (captureTrace) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
} from 'jest-matcher-utils';
|
} from 'jest-matcher-utils';
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { expectType, monotonicTime, pollUntilDeadline } from '../util';
|
import { expectType, pollUntilDeadline } from '../util';
|
||||||
|
|
||||||
export async function toBeTruthy<T>(
|
export async function toBeTruthy<T>(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
|
|
@ -42,16 +42,13 @@ export async function toBeTruthy<T>(
|
||||||
|
|
||||||
let received: T;
|
let received: T;
|
||||||
let pass = false;
|
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.
|
// TODO: interrupt on timeout for nice message.
|
||||||
await pollUntilDeadline(async () => {
|
await pollUntilDeadline(this, async remainingTime => {
|
||||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
|
||||||
received = await query(remainingTime);
|
received = await query(remainingTime);
|
||||||
pass = !!received;
|
pass = !!received;
|
||||||
return pass === !matcherOptions.isNot;
|
return pass === !matcherOptions.isNot;
|
||||||
}, deadline, 100);
|
}, options.timeout, 100);
|
||||||
|
|
||||||
const message = () => {
|
const message = () => {
|
||||||
return matcherHint(matcherName, undefined, '', matcherOptions);
|
return matcherHint(matcherName, undefined, '', matcherOptions);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from 'jest-matcher-utils';
|
} from 'jest-matcher-utils';
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import type { Expect } from '../types';
|
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.
|
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
||||||
const EXPECTED_LABEL = 'Expected';
|
const EXPECTED_LABEL = 'Expected';
|
||||||
|
|
@ -58,16 +58,13 @@ export async function toEqual<T>(
|
||||||
|
|
||||||
let received: T | undefined = undefined;
|
let received: T | undefined = undefined;
|
||||||
let pass = false;
|
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.
|
// TODO: interrupt on timeout for nice message.
|
||||||
await pollUntilDeadline(async () => {
|
await pollUntilDeadline(this, async remainingTime => {
|
||||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
|
||||||
received = await query(remainingTime);
|
received = await query(remainingTime);
|
||||||
pass = equals(received, expected, [iterableEquality]);
|
pass = equals(received, expected, [iterableEquality]);
|
||||||
return pass === !matcherOptions.isNot;
|
return pass === !matcherOptions.isNot;
|
||||||
}, deadline, 100);
|
}, options.timeout, 100);
|
||||||
|
|
||||||
const message = pass
|
const message = pass
|
||||||
? () =>
|
? () =>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
} from 'jest-matcher-utils';
|
} from 'jest-matcher-utils';
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { expectType, monotonicTime, pollUntilDeadline } from '../util';
|
import { expectType, pollUntilDeadline } from '../util';
|
||||||
|
|
||||||
export async function toMatchText(
|
export async function toMatchText(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
|
|
@ -68,12 +68,9 @@ export async function toMatchText(
|
||||||
|
|
||||||
let received: string;
|
let received: string;
|
||||||
let pass = false;
|
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.
|
// TODO: interrupt on timeout for nice message.
|
||||||
await pollUntilDeadline(async () => {
|
await pollUntilDeadline(this, async remainingTime => {
|
||||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
|
||||||
received = await query(remainingTime);
|
received = await query(remainingTime);
|
||||||
if (options.matchSubstring)
|
if (options.matchSubstring)
|
||||||
pass = received.includes(expected as string);
|
pass = received.includes(expected as string);
|
||||||
|
|
@ -83,7 +80,7 @@ export async function toMatchText(
|
||||||
pass = expected.test(received);
|
pass = expected.test(received);
|
||||||
|
|
||||||
return pass === !matcherOptions.isNot;
|
return pass === !matcherOptions.isNot;
|
||||||
}, deadline, 100);
|
}, options.timeout, 100);
|
||||||
|
|
||||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||||
const message = pass
|
const message = pass
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Expect } from './types';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { TestError, Location } from './types';
|
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;
|
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) {
|
while (true) {
|
||||||
const timeUntilDeadline = deadline ? deadline - monotonicTime() : Number.MAX_VALUE;
|
const remainingTime = deadline ? deadline - monotonicTime() : 1000 * 3600 * 24;
|
||||||
if (timeUntilDeadline <= 0)
|
if (remainingTime <= 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await func())
|
if (await func(remainingTime))
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof errors.TimeoutError)
|
if (e instanceof errors.TimeoutError)
|
||||||
return;
|
return;
|
||||||
throw e;
|
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.failed).toBe(1);
|
||||||
expect(result.exitCode).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.
|
* like [fixtures.viewport](https://playwright.dev/docs/api/class-fixtures#fixtures-viewport) take priority over this.
|
||||||
*/
|
*/
|
||||||
contextOptions: BrowserContextOptions;
|
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;
|
viewport: ViewportSize | null | undefined;
|
||||||
baseURL: string | undefined;
|
baseURL: string | undefined;
|
||||||
contextOptions: BrowserContextOptions;
|
contextOptions: BrowserContextOptions;
|
||||||
|
actionTimeout: number | undefined;
|
||||||
|
navigationTimeout: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue