mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: introduce expect.configure (#22533)
This commit is contained in:
parent
76a2afc836
commit
a1007bbe2c
@ -121,7 +121,17 @@ The same works with soft assertions:
|
||||
expect.soft(value, 'my soft assertion').toBe(56);
|
||||
```
|
||||
|
||||
## Polling
|
||||
## expect.configurte
|
||||
|
||||
You can create your own pre-configured `expect` instance to have its own
|
||||
defaults such as `timeout`, `soft` and `poll`.
|
||||
|
||||
```js
|
||||
const slowExpect = expect.configure({ timeout: 10000 });
|
||||
await slowExpect(locator).toHaveText('Submit);
|
||||
```
|
||||
|
||||
## expect.poll
|
||||
|
||||
You can convert any synchronous `expect` to an asynchronous polling one using `expect.poll`.
|
||||
|
||||
@ -152,7 +162,7 @@ await expect.poll(async () => {
|
||||
}).toBe(200);
|
||||
```
|
||||
|
||||
## Retrying
|
||||
## expect.toPass
|
||||
|
||||
You can retry blocks of code until they are passing successfully.
|
||||
|
||||
|
@ -34,10 +34,18 @@ export function currentlyLoadingFileSuite() {
|
||||
return currentFileSuite;
|
||||
}
|
||||
|
||||
let currentExpectConfigureTimeout: number | undefined;
|
||||
|
||||
export function setCurrentExpectConfigureTimeout(timeout: number | undefined) {
|
||||
currentExpectConfigureTimeout = timeout;
|
||||
}
|
||||
|
||||
export function currentExpectTimeout(options: { timeout?: number }) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (options.timeout !== undefined)
|
||||
return options.timeout;
|
||||
if (currentExpectConfigureTimeout !== undefined)
|
||||
return currentExpectConfigureTimeout;
|
||||
let defaultExpectTimeout = testInfo?._projectInternal?.expect?.timeout;
|
||||
if (typeof defaultExpectTimeout === 'undefined')
|
||||
defaultExpectTimeout = 5000;
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
captureRawStack,
|
||||
createAfterActionTraceEventForExpect,
|
||||
createBeforeActionTraceEventForExpect,
|
||||
isString,
|
||||
pollAgainstTimeout } from 'playwright-core/lib/utils';
|
||||
import type { ExpectZone } from 'playwright-core/lib/utils';
|
||||
import {
|
||||
@ -48,7 +49,7 @@ import {
|
||||
} from './matchers';
|
||||
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||
import type { Expect } from '../../types/test';
|
||||
import { currentTestInfo, currentExpectTimeout } from '../common/globals';
|
||||
import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals';
|
||||
import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util';
|
||||
import {
|
||||
expect as expectLibrary,
|
||||
@ -106,28 +107,58 @@ export const printReceivedStringContainExpectedResult = (
|
||||
|
||||
// #endregion
|
||||
|
||||
type ExpectMessageOrOptions = undefined | string | { message?: string, timeout?: number, intervals?: number[] };
|
||||
type ExpectMessage = string | { message?: string };
|
||||
|
||||
function createExpect(actual: unknown, messageOrOptions: ExpectMessageOrOptions, isSoft: boolean, isPoll: boolean, generator?: Generator): any {
|
||||
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(messageOrOptions, isSoft, isPoll, generator));
|
||||
function createMatchers(actual: unknown, info: ExpectMetaInfo): any {
|
||||
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info));
|
||||
}
|
||||
|
||||
export const expect: Expect = new Proxy(expectLibrary, {
|
||||
apply: function(target: any, thisArg: any, argumentsList: [actual: unknown, messageOrOptions: ExpectMessageOrOptions]) {
|
||||
const [actual, messageOrOptions] = argumentsList;
|
||||
return createExpect(actual, messageOrOptions, false /* isSoft */, false /* isPoll */);
|
||||
}
|
||||
});
|
||||
function createExpect(info: ExpectMetaInfo) {
|
||||
const expect: Expect = new Proxy(expectLibrary, {
|
||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||
const [actual, messageOrOptions] = argumentsList;
|
||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||
const newInfo = { ...info, message };
|
||||
if (newInfo.isPoll) {
|
||||
if (typeof actual !== 'function')
|
||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
||||
newInfo.generator = actual as any;
|
||||
}
|
||||
return createMatchers(actual, newInfo);
|
||||
}
|
||||
});
|
||||
|
||||
expect.soft = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => {
|
||||
return createExpect(actual, messageOrOptions, true /* isSoft */, false /* isPoll */);
|
||||
};
|
||||
expect.soft = (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
||||
return expect.configure({ soft: true })(actual, messageOrOptions) as any;
|
||||
};
|
||||
|
||||
expect.poll = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => {
|
||||
if (typeof actual !== 'function')
|
||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
||||
return createExpect(actual, messageOrOptions, false /* isSoft */, true /* isPoll */, actual as any);
|
||||
};
|
||||
expect.poll = (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
||||
const poll = isString(messageOrOptions) ? {} : messageOrOptions || {};
|
||||
return expect.configure({ poll })(actual, messageOrOptions) as any;
|
||||
};
|
||||
|
||||
expect.configure = (configuration: { message?: string, timeout?: number, soft?: boolean, poll?: boolean | { timeout?: number, intervals?: number[] } }) => {
|
||||
const newInfo = { ...info };
|
||||
if ('message' in configuration)
|
||||
newInfo.message = configuration.message;
|
||||
if ('timeout' in configuration)
|
||||
newInfo.timeout = configuration.timeout;
|
||||
if ('soft' in configuration)
|
||||
newInfo.isSoft = configuration.soft;
|
||||
if ('poll' in configuration) {
|
||||
newInfo.isPoll = !!configuration.poll;
|
||||
if (typeof configuration.poll === 'object') {
|
||||
newInfo.pollTimeout = configuration.poll.timeout;
|
||||
newInfo.pollIntervals = configuration.poll.intervals;
|
||||
}
|
||||
}
|
||||
return createExpect(newInfo);
|
||||
};
|
||||
|
||||
return expect;
|
||||
}
|
||||
|
||||
export const expect: Expect = createExpect({});
|
||||
|
||||
expectLibrary.setState({ expand: false });
|
||||
|
||||
@ -168,10 +199,10 @@ type Generator = () => any;
|
||||
|
||||
type ExpectMetaInfo = {
|
||||
message?: string;
|
||||
isNot: boolean;
|
||||
isSoft: boolean;
|
||||
isPoll: boolean;
|
||||
nameTokens: string[];
|
||||
isNot?: boolean;
|
||||
isSoft?: boolean;
|
||||
isPoll?: boolean;
|
||||
timeout?: number;
|
||||
pollTimeout?: number;
|
||||
pollIntervals?: number[];
|
||||
generator?: Generator;
|
||||
@ -180,15 +211,8 @@ type ExpectMetaInfo = {
|
||||
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
private _info: ExpectMetaInfo;
|
||||
|
||||
constructor(messageOrOptions: ExpectMessageOrOptions, isSoft: boolean, isPoll: boolean, generator?: Generator) {
|
||||
this._info = { isSoft, isPoll, generator, isNot: false, nameTokens: [] };
|
||||
if (typeof messageOrOptions === 'string') {
|
||||
this._info.message = messageOrOptions;
|
||||
} else {
|
||||
this._info.message = messageOrOptions?.message;
|
||||
this._info.pollTimeout = messageOrOptions?.timeout;
|
||||
this._info.pollIntervals = messageOrOptions?.intervals;
|
||||
}
|
||||
constructor(info: ExpectMetaInfo) {
|
||||
this._info = { ...info };
|
||||
}
|
||||
|
||||
get(target: Object, matcherName: string | symbol, receiver: any): any {
|
||||
@ -205,7 +229,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
if (this._info.isPoll) {
|
||||
if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
|
||||
throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
|
||||
matcher = (...args: any[]) => pollMatcher(matcherName, this._info.isNot, this._info.pollIntervals, currentExpectTimeout({ timeout: this._info.pollTimeout }), this._info.generator!, ...args);
|
||||
matcher = (...args: any[]) => pollMatcher(matcherName, !!this._info.isNot, this._info.pollIntervals, currentExpectTimeout({ timeout: this._info.pollTimeout }), this._info.generator!, ...args);
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const testInfo = currentTestInfo();
|
||||
@ -278,6 +302,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
try {
|
||||
const expectZone: ExpectZone = { title: defaultTitle, wallTime };
|
||||
await zones.run<ExpectZone, any>('expectZone', expectZone, async () => {
|
||||
// We assume that the matcher will read the current expect timeout the first thing.
|
||||
setCurrentExpectConfigureTimeout(this._info.timeout);
|
||||
await matcher.call(target, ...args);
|
||||
});
|
||||
finalizer();
|
||||
|
6
packages/playwright-test/types/test.d.ts
vendored
6
packages/playwright-test/types/test.d.ts
vendored
@ -4654,6 +4654,12 @@ export type Expect = {
|
||||
not: BaseMatchers<Promise<void>, T>;
|
||||
};
|
||||
extend(matchers: any): void;
|
||||
configure: (configuration: {
|
||||
message?: string,
|
||||
timeout?: number,
|
||||
soft?: boolean,
|
||||
poll?: boolean | { timeout?: number, intervals?: number[] },
|
||||
}) => Expect;
|
||||
getState(): {
|
||||
expand?: boolean;
|
||||
isNot: boolean;
|
||||
|
160
tests/playwright-test/expect-configure.spec.ts
Normal file
160
tests/playwright-test/expect-configure.spec.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
|
||||
test('should configure timeout', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const fastExpect = expect.configure({ timeout: 1 });
|
||||
test('pass', async ({ page }) => {
|
||||
const time = performance.now();
|
||||
try {
|
||||
await fastExpect(page.locator('li')).toBeVisible();
|
||||
} catch (e) {
|
||||
expect(performance.now() - time).toBeLessThan(5000);
|
||||
}
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('should configure message', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'expect-test.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const namedExpect = expect.configure({ message: 'x-foo must be visible' });
|
||||
test('custom expect message', async ({page}) => {
|
||||
await namedExpect(page.locator('x-foo')).toBeVisible({timeout: 1});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain([
|
||||
` Error: x-foo must be visible`,
|
||||
``,
|
||||
` Call log:`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('should prefer local message', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'expect-test.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const namedExpect = expect.configure({ message: 'x-foo must be visible' });
|
||||
test('custom expect message', async ({page}) => {
|
||||
await namedExpect(page.locator('x-foo'), { message: 'overridden' }).toBeVisible({timeout: 1});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain([
|
||||
` Error: overridden`,
|
||||
``,
|
||||
` Call log:`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('should configure soft', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const softExpect = expect.configure({ soft: true });
|
||||
test('should work', () => {
|
||||
softExpect(1+1).toBe(3);
|
||||
console.log('woof-woof');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('woof-woof');
|
||||
});
|
||||
|
||||
test('should configure poll', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const pollingExpect = expect.configure({ poll: { timeout: 1000, intervals: [0, 10000] } });
|
||||
test('should fail', async () => {
|
||||
let probes = 0;
|
||||
const startTime = Date.now();
|
||||
await pollingExpect(() => ++probes).toBe(3).catch(() => {});
|
||||
// Probe at 0 and epsilon.
|
||||
expect(probes).toBe(2);
|
||||
expect(Date.now() - startTime).toBeLessThan(5000);
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should chain configure', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'expect-test.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const slowExpect = expect.configure({ timeout: 1 });
|
||||
const slowAndSoftExpect = slowExpect.configure({ soft: true });
|
||||
test('custom expect message', async ({page}) => {
|
||||
await slowAndSoftExpect(page.locator('x-foo')).toBeVisible({timeout: 1});
|
||||
console.log('%% woof-woof');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.outputLines).toEqual(['woof-woof']);
|
||||
});
|
||||
|
||||
test('should cancel effect', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const softExpect = expect.configure({ soft: true });
|
||||
const normalExpect = expect.configure({ soft: false });
|
||||
test('should work', () => {
|
||||
normalExpect(1+1).toBe(3);
|
||||
console.log('%% woof-woof');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.outputLines).toEqual([]);
|
||||
});
|
||||
|
||||
test('should configure soft poll', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
const pollingExpect = expect.configure({ soft: true, poll: { timeout: 1000, intervals: [0, 10000] } });
|
||||
test('should fail', async () => {
|
||||
let probes = 0;
|
||||
const startTime = Date.now();
|
||||
await pollingExpect(() => ++probes).toBe(3);
|
||||
// Probe at 0 and epsilon.
|
||||
expect(probes).toBe(2);
|
||||
expect(Date.now() - startTime).toBeLessThan(5000);
|
||||
console.log('%% woof-woof');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.outputLines).toEqual(['woof-woof']);
|
||||
});
|
@ -36,12 +36,12 @@ test('soft expects should work', async ({ runInlineTest }) => {
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('should work', () => {
|
||||
test.expect.soft(1+1).toBe(3);
|
||||
console.log('woof-woof');
|
||||
console.log('%% woof-woof');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('woof-woof');
|
||||
expect(result.outputLines).toEqual(['woof-woof']);
|
||||
});
|
||||
|
||||
test('should report a mixture of soft and non-soft errors', async ({ runInlineTest }) => {
|
||||
|
6
utils/generate_types/overrides-test.d.ts
vendored
6
utils/generate_types/overrides-test.d.ts
vendored
@ -342,6 +342,12 @@ export type Expect = {
|
||||
not: BaseMatchers<Promise<void>, T>;
|
||||
};
|
||||
extend(matchers: any): void;
|
||||
configure: (configuration: {
|
||||
message?: string,
|
||||
timeout?: number,
|
||||
soft?: boolean,
|
||||
poll?: boolean | { timeout?: number, intervals?: number[] },
|
||||
}) => Expect;
|
||||
getState(): {
|
||||
expand?: boolean;
|
||||
isNot: boolean;
|
||||
|
Loading…
x
Reference in New Issue
Block a user