feat(expect): allow chaining expects (#27248)
This commit is contained in:
parent
49fd9500fe
commit
a6a0257c88
|
|
@ -48,7 +48,7 @@ import {
|
||||||
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||||
import type { Expect } from '../../types/test';
|
import type { Expect } from '../../types/test';
|
||||||
import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals';
|
import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals';
|
||||||
import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util';
|
import { filteredStackTrace, stringifyStackFrames, trimLongString } from '../util';
|
||||||
import {
|
import {
|
||||||
expect as expectLibrary,
|
expect as expectLibrary,
|
||||||
INVERTED_COLOR,
|
INVERTED_COLOR,
|
||||||
|
|
@ -106,7 +106,7 @@ function createMatchers(actual: unknown, info: ExpectMetaInfo): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createExpect(info: ExpectMetaInfo) {
|
function createExpect(info: ExpectMetaInfo) {
|
||||||
const expectInstance: Expect = new Proxy(expectLibrary, {
|
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
||||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||||
const [actual, messageOrOptions] = argumentsList;
|
const [actual, messageOrOptions] = argumentsList;
|
||||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||||
|
|
@ -123,6 +123,13 @@ function createExpect(info: ExpectMetaInfo) {
|
||||||
if (property === 'configure')
|
if (property === 'configure')
|
||||||
return configure;
|
return configure;
|
||||||
|
|
||||||
|
if (property === 'extend') {
|
||||||
|
return (matchers: any) => {
|
||||||
|
expectLibrary.extend(matchers);
|
||||||
|
return expectInstance;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (property === 'soft') {
|
if (property === 'soft') {
|
||||||
return (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
return (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
||||||
return configure({ soft: true })(actual, messageOrOptions) as any;
|
return configure({ soft: true })(actual, messageOrOptions) as any;
|
||||||
|
|
@ -160,7 +167,7 @@ function createExpect(info: ExpectMetaInfo) {
|
||||||
return expectInstance;
|
return expectInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expect: Expect = createExpect({});
|
export const expect: Expect<{}> = createExpect({});
|
||||||
|
|
||||||
expectLibrary.setState({ expand: false });
|
expectLibrary.setState({ expand: false });
|
||||||
|
|
||||||
|
|
@ -269,9 +276,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializedError = serializeError(jestError);
|
// Use the exact stack that we entered the matcher with.
|
||||||
// Serialized error has filtered stack trace.
|
jestError.stack = jestError.name + ': ' + jestError.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||||
jestError.stack = serializedError.stack;
|
const serializedError = {
|
||||||
|
message: jestError.message,
|
||||||
|
stack: jestError.stack,
|
||||||
|
};
|
||||||
|
|
||||||
step?.complete({ error: serializedError });
|
step?.complete({ error: serializedError });
|
||||||
if (this._info.isSoft)
|
if (this._info.isSoft)
|
||||||
testInfo._failWithError(serializedError, false /* isHardError */);
|
testInfo._failWithError(serializedError, false /* isHardError */);
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,5 @@ export async function toBeTruthy(
|
||||||
return matches ? `${header}Expected: not ${expected}\nReceived: ${expected}${logText}` :
|
return matches ? `${header}Expected: not ${expected}\nReceived: ${expected}${logText}` :
|
||||||
`${header}Expected: ${expected}\nReceived: ${unexpected}${logText}`;
|
`${header}Expected: ${expected}\nReceived: ${unexpected}${logText}`;
|
||||||
};
|
};
|
||||||
return { locator: receiver, message, pass: matches, actual, name: matcherName, expected, log };
|
return { message, pass: matches, actual, name: matcherName, expected, log };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,5 +67,5 @@ export async function toEqual<T>(
|
||||||
// Passing the actual and expected objects so that a custom reporter
|
// Passing the actual and expected objects so that a custom reporter
|
||||||
// could access them, for example in order to display a custom visual diff,
|
// could access them, for example in order to display a custom visual diff,
|
||||||
// or create a different error message
|
// or create a different error message
|
||||||
return { locator: receiver, actual: received, expected, message, name: matcherName, pass, log };
|
return { actual: received, expected, message, name: matcherName, pass, log };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ export async function toMatchText(
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locator: receiver,
|
|
||||||
name: matcherName,
|
name: matcherName,
|
||||||
expected,
|
expected,
|
||||||
message,
|
message,
|
||||||
|
|
|
||||||
67
packages/playwright/types/test.d.ts
vendored
67
packages/playwright/types/test.d.ts
vendored
|
|
@ -3363,7 +3363,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
expect: Expect;
|
expect: Expect<{}>;
|
||||||
/**
|
/**
|
||||||
* Extends the `test` object by defining fixtures and/or options that can be used in the tests.
|
* Extends the `test` object by defining fixtures and/or options that can be used in the tests.
|
||||||
*
|
*
|
||||||
|
|
@ -5083,38 +5083,81 @@ type AllMatchers<R, T> = PageAssertions & LocatorAssertions & APIResponseAsserti
|
||||||
|
|
||||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
||||||
type MakeMatchers<R, T> = {
|
type ToUserMatcher<F> = F extends (first: any, ...args: infer Rest) => infer R ? (...args: Rest) => (R extends PromiseLike<infer U> ? Promise<void> : void) : never;
|
||||||
|
type ToUserMatcherObject<T, ArgType> = {
|
||||||
|
[K in keyof T as T[K] extends (arg: ArgType, ...rest: any[]) => any ? K : never]: ToUserMatcher<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MatcherHintColor = (arg: string) => string;
|
||||||
|
|
||||||
|
export type MatcherHintOptions = {
|
||||||
|
comment?: string;
|
||||||
|
expectedColor?: MatcherHintColor;
|
||||||
|
isDirectExpectCall?: boolean;
|
||||||
|
isNot?: boolean;
|
||||||
|
promise?: string;
|
||||||
|
receivedColor?: MatcherHintColor;
|
||||||
|
secondArgument?: string;
|
||||||
|
secondArgumentColor?: MatcherHintColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ExpectMatcherUtils {
|
||||||
|
matcherHint(matcherName: string, received: unknown, expected: unknown, options?: MatcherHintOptions): string;
|
||||||
|
printDiffOrStringify(expected: unknown, received: unknown, expectedLabel: string, receivedLabel: string, expand: boolean): string;
|
||||||
|
printExpected(value: unknown): string;
|
||||||
|
printReceived(object: unknown): string;
|
||||||
|
printWithType<T>(name: string, value: T, print: (value: T) => string): string;
|
||||||
|
diff(a: unknown, b: unknown): string | null;
|
||||||
|
stringify(object: unknown, maxDepth?: number, maxWidth?: number): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isNot: boolean;
|
||||||
|
promise: 'rejects' | 'resolves' | '';
|
||||||
|
utils: ExpectMatcherUtils;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MatcherReturnType = {
|
||||||
|
message: () => string;
|
||||||
|
pass: boolean;
|
||||||
|
name?: string;
|
||||||
|
expected?: unknown;
|
||||||
|
actual?: any;
|
||||||
|
log?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MakeMatchers<R, T, ExtendedMatchers> = {
|
||||||
/**
|
/**
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
*/
|
*/
|
||||||
not: MakeMatchers<R, T>;
|
not: MakeMatchers<R, T, ExtendedMatchers>;
|
||||||
/**
|
/**
|
||||||
* Use resolves to unwrap the value of a fulfilled promise so any other
|
* Use resolves to unwrap the value of a fulfilled promise so any other
|
||||||
* matcher can be chained. If the promise is rejected the assertion fails.
|
* matcher can be chained. If the promise is rejected the assertion fails.
|
||||||
*/
|
*/
|
||||||
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
resolves: MakeMatchers<Promise<R>, Awaited<T>, ExtendedMatchers>;
|
||||||
/**
|
/**
|
||||||
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
||||||
* If the promise is fulfilled the assertion fails.
|
* If the promise is fulfilled the assertion fails.
|
||||||
*/
|
*/
|
||||||
rejects: MakeMatchers<Promise<R>, any>;
|
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
|
||||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>;
|
||||||
|
|
||||||
export type Expect = {
|
export type Expect<ExtendedMatchers> = {
|
||||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>;
|
||||||
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
|
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T, ExtendedMatchers>;
|
||||||
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers<Promise<void>, T> & {
|
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers<Promise<void>, T> & {
|
||||||
/**
|
/**
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
*/
|
*/
|
||||||
not: BaseMatchers<Promise<void>, T>;
|
not: BaseMatchers<Promise<void>, T>;
|
||||||
};
|
};
|
||||||
extend(matchers: any): void;
|
extend<MoreMatchers extends Record<string, (this: State, receiver: any, ...args: any[]) => MatcherReturnType | Promise<MatcherReturnType>>>(matchers: MoreMatchers): Expect<ExtendedMatchers & MoreMatchers>;
|
||||||
configure: (configuration: {
|
configure: (configuration: {
|
||||||
message?: string,
|
message?: string,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
soft?: boolean,
|
soft?: boolean,
|
||||||
}) => Expect;
|
}) => Expect<ExtendedMatchers>;
|
||||||
getState(): {
|
getState(): {
|
||||||
expand?: boolean;
|
expand?: boolean;
|
||||||
isNot?: boolean;
|
isNot?: boolean;
|
||||||
|
|
@ -5141,7 +5184,7 @@ export const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions, Playwrig
|
||||||
export default test;
|
export default test;
|
||||||
|
|
||||||
export const _baseTest: TestType<{}, {}>;
|
export const _baseTest: TestType<{}, {}>;
|
||||||
export const expect: Expect;
|
export const expect: Expect<{}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines Playwright config
|
* Defines Playwright config
|
||||||
|
|
|
||||||
25
tests/installation/expect.d.ts
vendored
25
tests/installation/expect.d.ts
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace PlaywrightTest {
|
|
||||||
interface Matchers<R, T> {
|
|
||||||
toHaveLoggedSoftwareDownload(browsers: ("chromium" | "firefox" | "webkit" | "ffmpeg")[]): R;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,9 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
|
||||||
/// <reference path="./expect.d.ts" />
|
|
||||||
|
|
||||||
import { _baseTest as _test, expect as _expect } from '@playwright/test';
|
import { _baseTest as _test, expect as _expect } from '@playwright/test';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
@ -32,7 +29,7 @@ export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os
|
||||||
|
|
||||||
const debug = debugLogger('itest');
|
const debug = debugLogger('itest');
|
||||||
|
|
||||||
_expect.extend({
|
const expect = _expect.extend({
|
||||||
toHaveLoggedSoftwareDownload(received: any, browsers: ('chromium' | 'firefox' | 'webkit' | 'ffmpeg')[]) {
|
toHaveLoggedSoftwareDownload(received: any, browsers: ('chromium' | 'firefox' | 'webkit' | 'ffmpeg')[]) {
|
||||||
if (typeof received !== 'string')
|
if (typeof received !== 'string')
|
||||||
throw new Error(`Expected argument to be a string.`);
|
throw new Error(`Expected argument to be a string.`);
|
||||||
|
|
@ -42,8 +39,12 @@ _expect.extend({
|
||||||
downloaded.add(browser.toLowerCase());
|
downloaded.add(browser.toLowerCase());
|
||||||
|
|
||||||
const expected = browsers;
|
const expected = browsers;
|
||||||
if (expected.length === downloaded.size && expected.every(browser => downloaded.has(browser)))
|
if (expected.length === downloaded.size && expected.every(browser => downloaded.has(browser))) {
|
||||||
return { pass: true };
|
return {
|
||||||
|
pass: true,
|
||||||
|
message: () => 'Expected not to download browsers, but did.'
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
message: () => [
|
message: () => [
|
||||||
|
|
@ -55,8 +56,6 @@ _expect.extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const expect = _expect;
|
|
||||||
|
|
||||||
type ExecOptions = { cwd?: string, env?: Record<string, string>, message?: string, expectToExitWithError?: boolean };
|
type ExecOptions = { cwd?: string, env?: Record<string, string>, message?: string, expectToExitWithError?: boolean };
|
||||||
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
|
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
|
||||||
|
|
||||||
|
|
@ -199,7 +198,7 @@ export const test = _test
|
||||||
},
|
},
|
||||||
tsc: async ({ exec }, use) => {
|
tsc: async ({ exec }, use) => {
|
||||||
await exec('npm i --foreground-scripts typescript@3.8 @types/node@14');
|
await exec('npm i --foreground-scripts typescript@3.8 @types/node@14');
|
||||||
await use((...args: ArgsOrOptions) => exec('npx', '-p', 'typescript@3.8', 'tsc', ...args));
|
await use((...args: ArgsOrOptions) => exec('npx', '-p', 'typescript@4.1.6', 'tsc', ...args));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ test('toMatchText-based assertions should have matcher result', async ({ page })
|
||||||
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text2/,
|
expected: /Text2/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
||||||
|
|
@ -47,7 +46,6 @@ Call log`);
|
||||||
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text/,
|
expected: /Text/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
||||||
|
|
@ -73,7 +71,6 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page })
|
||||||
const e = await expect(page.locator('#node2')).toBeVisible({ timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node2')).toBeVisible({ timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'hidden',
|
actual: 'hidden',
|
||||||
expected: 'visible',
|
expected: 'visible',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeVisible()`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeVisible()`),
|
||||||
|
|
@ -95,7 +92,6 @@ Call log`);
|
||||||
const e = await expect(page.locator('#node')).not.toBeVisible({ timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node')).not.toBeVisible({ timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'visible',
|
actual: 'visible',
|
||||||
expected: 'visible',
|
expected: 'visible',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeVisible()`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeVisible()`),
|
||||||
|
|
@ -121,7 +117,6 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||||
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 0,
|
actual: 0,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveCount(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveCount(expected)`),
|
||||||
|
|
@ -142,7 +137,6 @@ Call log`);
|
||||||
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 1,
|
actual: 1,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveCount(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveCount(expected)`),
|
||||||
|
|
@ -171,7 +165,6 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag
|
||||||
const e = await expect(page.locator('#unchecked')).toBeChecked({ timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#unchecked')).toBeChecked({ timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'unchecked',
|
actual: 'unchecked',
|
||||||
expected: 'checked',
|
expected: 'checked',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked()`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked()`),
|
||||||
|
|
@ -193,7 +186,6 @@ Call log`);
|
||||||
const e = await expect(page.locator('#checked')).not.toBeChecked({ timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#checked')).not.toBeChecked({ timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'checked',
|
actual: 'checked',
|
||||||
expected: 'checked',
|
expected: 'checked',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked()`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked()`),
|
||||||
|
|
@ -215,7 +207,6 @@ Call log`);
|
||||||
const e = await expect(page.locator('#checked')).toBeChecked({ checked: false, timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#checked')).toBeChecked({ checked: false, timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'checked',
|
actual: 'checked',
|
||||||
expected: 'unchecked',
|
expected: 'unchecked',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked({ checked: false })`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked({ checked: false })`),
|
||||||
|
|
@ -237,7 +228,6 @@ Call log`);
|
||||||
const e = await expect(page.locator('#unchecked')).not.toBeChecked({ checked: false, timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#unchecked')).not.toBeChecked({ checked: false, timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
locator: expect.any(Object),
|
|
||||||
actual: 'unchecked',
|
actual: 'unchecked',
|
||||||
expected: 'unchecked',
|
expected: 'unchecked',
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false })`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false })`),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
|
import { test, expect, parseTestRunnerOutput, stripAnsi } from './playwright-test-fixtures';
|
||||||
|
|
||||||
test('should be able to call expect.extend in config', async ({ runInlineTest }) => {
|
test('should be able to call expect.extend in config', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
@ -273,7 +273,12 @@ test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect, type Page, type APIResponse } from '@playwright/test';
|
import { test, expect, type Page, type APIResponse } from '@playwright/test';
|
||||||
test.expect.extend({
|
test.expect.extend({
|
||||||
toBeWithinRange() { },
|
toBeWithinRange() {
|
||||||
|
return {
|
||||||
|
pass: true,
|
||||||
|
message: () => '',
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = {} as Page;
|
const page = {} as Page;
|
||||||
|
|
@ -704,3 +709,153 @@ test('should not leak long expect message strings', async ({ runInlineTest }) =>
|
||||||
expect(result.failed).toBe(0);
|
expect(result.failed).toBe(0);
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should chain expect matchers and expose matcher utils (TSC)', async ({ runTSC }) => {
|
||||||
|
const result = await runTSC({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect as baseExpect } from '@playwright/test';
|
||||||
|
import type { Page, Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
function callLogText(log: string[] | undefined): string {
|
||||||
|
if (!log)
|
||||||
|
return '';
|
||||||
|
return log.join('\\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect = baseExpect.extend({
|
||||||
|
async toHaveAmount(locator: Locator, expected: string, options?: { timeout?: number }) {
|
||||||
|
const baseAmount = locator.locator('.base-amount');
|
||||||
|
|
||||||
|
let pass: boolean;
|
||||||
|
let matcherResult: any;
|
||||||
|
try {
|
||||||
|
await baseExpect(baseAmount).toHaveAttribute('data-amount', expected, options);
|
||||||
|
pass = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
matcherResult = e.matcherResult;
|
||||||
|
pass = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectOptions = {
|
||||||
|
isNot: this.isNot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = callLogText(matcherResult?.log);
|
||||||
|
const message = pass
|
||||||
|
? () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||||
|
'\\n\\n' +
|
||||||
|
\`Expected: \${this.isNot ? 'not' : ''}\${this.utils.printExpected(expected)}\\n\` +
|
||||||
|
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||||
|
log
|
||||||
|
: () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||||
|
'\\n\\n' +
|
||||||
|
\`Expected: \${this.utils.printExpected(expected)}\n\` +
|
||||||
|
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||||
|
log;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'toHaveAmount',
|
||||||
|
expected,
|
||||||
|
message,
|
||||||
|
pass,
|
||||||
|
actual: matcherResult?.actual,
|
||||||
|
log: matcherResult?.log,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeANicePage(page: Page) {
|
||||||
|
return {
|
||||||
|
name: 'toBeANicePage',
|
||||||
|
expected: 1,
|
||||||
|
message: () => '',
|
||||||
|
pass: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom matchers', async ({ page }) => {
|
||||||
|
await page.setContent(\`
|
||||||
|
<div>
|
||||||
|
<div class='base-amount' data-amount='2'></div>
|
||||||
|
</div>
|
||||||
|
\`);
|
||||||
|
await expect(page.locator('div')).toHaveAmount('3', { timeout: 1000 });
|
||||||
|
await expect(page).toBeANicePage();
|
||||||
|
// @ts-expect-error
|
||||||
|
await expect(page).toHaveAmount('3', { timeout: 1000 });
|
||||||
|
// @ts-expect-error
|
||||||
|
await expect(page.locator('div')).toBeANicePage();
|
||||||
|
});`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should chain expect matchers and expose matcher utils', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect as baseExpect } from '@playwright/test';
|
||||||
|
import type { Page, Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
function callLogText(log: string[] | undefined): string {
|
||||||
|
if (!log)
|
||||||
|
return '';
|
||||||
|
return log.join('\\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect = baseExpect.extend({
|
||||||
|
async toHaveAmount(locator: Locator, expected: string, options?: { timeout?: number }) {
|
||||||
|
const baseAmount = locator.locator('.base-amount');
|
||||||
|
|
||||||
|
let pass: boolean;
|
||||||
|
let matcherResult: any;
|
||||||
|
try {
|
||||||
|
await baseExpect(baseAmount).toHaveAttribute('data-amount', expected, options);
|
||||||
|
pass = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
matcherResult = e.matcherResult;
|
||||||
|
pass = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectOptions = {
|
||||||
|
isNot: this.isNot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = callLogText(matcherResult?.log);
|
||||||
|
const message = pass
|
||||||
|
? () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||||
|
'\\n\\n' +
|
||||||
|
\`Expected: \${this.isNot ? 'not' : ''}\${this.utils.printExpected(expected)}\\n\` +
|
||||||
|
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||||
|
log
|
||||||
|
: () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||||
|
'\\n\\n' +
|
||||||
|
\`Expected: \${this.utils.printExpected(expected)}\n\` +
|
||||||
|
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||||
|
log;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'toHaveAmount',
|
||||||
|
expected,
|
||||||
|
message,
|
||||||
|
pass,
|
||||||
|
actual: matcherResult?.actual,
|
||||||
|
log: matcherResult?.log,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom matchers', async ({ page }) => {
|
||||||
|
await page.setContent(\`
|
||||||
|
<div>
|
||||||
|
<div class='base-amount' data-amount='2'></div>
|
||||||
|
</div>
|
||||||
|
\`);
|
||||||
|
await expect(page.locator('div')).toHaveAmount('3', { timeout: 1000 });
|
||||||
|
});`
|
||||||
|
}, { workers: 1 });
|
||||||
|
const output = stripAnsi(result.output);
|
||||||
|
expect(output).toContain(`await expect(page.locator('div')).toHaveAmount('3', { timeout: 1000 });`);
|
||||||
|
expect(output).toContain('a.spec.ts:60');
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -600,6 +600,7 @@ class TypesGenerator {
|
||||||
'PlaywrightWorkerOptions.defaultBrowserType',
|
'PlaywrightWorkerOptions.defaultBrowserType',
|
||||||
'PlaywrightWorkerArgs.playwright',
|
'PlaywrightWorkerArgs.playwright',
|
||||||
'Matchers',
|
'Matchers',
|
||||||
|
'ExpectMatcherUtils',
|
||||||
]),
|
]),
|
||||||
doNotExportClassNames: new Set([...assertionClasses, 'TestProject']),
|
doNotExportClassNames: new Set([...assertionClasses, 'TestProject']),
|
||||||
includeExperimental,
|
includeExperimental,
|
||||||
|
|
|
||||||
67
utils/generate_types/overrides-test.d.ts
vendored
67
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -158,7 +158,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
||||||
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||||
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||||
step<T>(title: string, body: () => T | Promise<T>): Promise<T>;
|
step<T>(title: string, body: () => T | Promise<T>): Promise<T>;
|
||||||
expect: Expect;
|
expect: Expect<{}>;
|
||||||
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||||
info(): TestInfo;
|
info(): TestInfo;
|
||||||
}
|
}
|
||||||
|
|
@ -341,38 +341,81 @@ type AllMatchers<R, T> = PageAssertions & LocatorAssertions & APIResponseAsserti
|
||||||
|
|
||||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
|
||||||
type MakeMatchers<R, T> = {
|
type ToUserMatcher<F> = F extends (first: any, ...args: infer Rest) => infer R ? (...args: Rest) => (R extends PromiseLike<infer U> ? Promise<void> : void) : never;
|
||||||
|
type ToUserMatcherObject<T, ArgType> = {
|
||||||
|
[K in keyof T as T[K] extends (arg: ArgType, ...rest: any[]) => any ? K : never]: ToUserMatcher<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MatcherHintColor = (arg: string) => string;
|
||||||
|
|
||||||
|
export type MatcherHintOptions = {
|
||||||
|
comment?: string;
|
||||||
|
expectedColor?: MatcherHintColor;
|
||||||
|
isDirectExpectCall?: boolean;
|
||||||
|
isNot?: boolean;
|
||||||
|
promise?: string;
|
||||||
|
receivedColor?: MatcherHintColor;
|
||||||
|
secondArgument?: string;
|
||||||
|
secondArgumentColor?: MatcherHintColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ExpectMatcherUtils {
|
||||||
|
matcherHint(matcherName: string, received: unknown, expected: unknown, options?: MatcherHintOptions): string;
|
||||||
|
printDiffOrStringify(expected: unknown, received: unknown, expectedLabel: string, receivedLabel: string, expand: boolean): string;
|
||||||
|
printExpected(value: unknown): string;
|
||||||
|
printReceived(object: unknown): string;
|
||||||
|
printWithType<T>(name: string, value: T, print: (value: T) => string): string;
|
||||||
|
diff(a: unknown, b: unknown): string | null;
|
||||||
|
stringify(object: unknown, maxDepth?: number, maxWidth?: number): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isNot: boolean;
|
||||||
|
promise: 'rejects' | 'resolves' | '';
|
||||||
|
utils: ExpectMatcherUtils;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MatcherReturnType = {
|
||||||
|
message: () => string;
|
||||||
|
pass: boolean;
|
||||||
|
name?: string;
|
||||||
|
expected?: unknown;
|
||||||
|
actual?: any;
|
||||||
|
log?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MakeMatchers<R, T, ExtendedMatchers> = {
|
||||||
/**
|
/**
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
*/
|
*/
|
||||||
not: MakeMatchers<R, T>;
|
not: MakeMatchers<R, T, ExtendedMatchers>;
|
||||||
/**
|
/**
|
||||||
* Use resolves to unwrap the value of a fulfilled promise so any other
|
* Use resolves to unwrap the value of a fulfilled promise so any other
|
||||||
* matcher can be chained. If the promise is rejected the assertion fails.
|
* matcher can be chained. If the promise is rejected the assertion fails.
|
||||||
*/
|
*/
|
||||||
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
resolves: MakeMatchers<Promise<R>, Awaited<T>, ExtendedMatchers>;
|
||||||
/**
|
/**
|
||||||
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
||||||
* If the promise is fulfilled the assertion fails.
|
* If the promise is fulfilled the assertion fails.
|
||||||
*/
|
*/
|
||||||
rejects: MakeMatchers<Promise<R>, any>;
|
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
|
||||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>;
|
||||||
|
|
||||||
export type Expect = {
|
export type Expect<ExtendedMatchers> = {
|
||||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>;
|
||||||
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
|
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T, ExtendedMatchers>;
|
||||||
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers<Promise<void>, T> & {
|
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers<Promise<void>, T> & {
|
||||||
/**
|
/**
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
*/
|
*/
|
||||||
not: BaseMatchers<Promise<void>, T>;
|
not: BaseMatchers<Promise<void>, T>;
|
||||||
};
|
};
|
||||||
extend(matchers: any): void;
|
extend<MoreMatchers extends Record<string, (this: State, receiver: any, ...args: any[]) => MatcherReturnType | Promise<MatcherReturnType>>>(matchers: MoreMatchers): Expect<ExtendedMatchers & MoreMatchers>;
|
||||||
configure: (configuration: {
|
configure: (configuration: {
|
||||||
message?: string,
|
message?: string,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
soft?: boolean,
|
soft?: boolean,
|
||||||
}) => Expect;
|
}) => Expect<ExtendedMatchers>;
|
||||||
getState(): {
|
getState(): {
|
||||||
expand?: boolean;
|
expand?: boolean;
|
||||||
isNot?: boolean;
|
isNot?: boolean;
|
||||||
|
|
@ -399,7 +442,7 @@ export const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions, Playwrig
|
||||||
export default test;
|
export default test;
|
||||||
|
|
||||||
export const _baseTest: TestType<{}, {}>;
|
export const _baseTest: TestType<{}, {}>;
|
||||||
export const expect: Expect;
|
export const expect: Expect<{}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines Playwright config
|
* Defines Playwright config
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue