feat(expect): allow chaining expects (#27248)
This commit is contained in:
Родитель
49fd9500fe
Коммит
a6a0257c88
|
@ -48,7 +48,7 @@ import {
|
|||
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||
import type { Expect } from '../../types/test';
|
||||
import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals';
|
||||
import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util';
|
||||
import { filteredStackTrace, stringifyStackFrames, trimLongString } from '../util';
|
||||
import {
|
||||
expect as expectLibrary,
|
||||
INVERTED_COLOR,
|
||||
|
@ -106,7 +106,7 @@ function createMatchers(actual: unknown, info: ExpectMetaInfo): any {
|
|||
}
|
||||
|
||||
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?]) {
|
||||
const [actual, messageOrOptions] = argumentsList;
|
||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||
|
@ -123,6 +123,13 @@ function createExpect(info: ExpectMetaInfo) {
|
|||
if (property === 'configure')
|
||||
return configure;
|
||||
|
||||
if (property === 'extend') {
|
||||
return (matchers: any) => {
|
||||
expectLibrary.extend(matchers);
|
||||
return expectInstance;
|
||||
};
|
||||
}
|
||||
|
||||
if (property === 'soft') {
|
||||
return (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
||||
return configure({ soft: true })(actual, messageOrOptions) as any;
|
||||
|
@ -160,7 +167,7 @@ function createExpect(info: ExpectMetaInfo) {
|
|||
return expectInstance;
|
||||
}
|
||||
|
||||
export const expect: Expect = createExpect({});
|
||||
export const expect: Expect<{}> = createExpect({});
|
||||
|
||||
expectLibrary.setState({ expand: false });
|
||||
|
||||
|
@ -269,9 +276,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
}
|
||||
|
||||
const serializedError = serializeError(jestError);
|
||||
// Serialized error has filtered stack trace.
|
||||
jestError.stack = serializedError.stack;
|
||||
// Use the exact stack that we entered the matcher with.
|
||||
jestError.stack = jestError.name + ': ' + jestError.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
const serializedError = {
|
||||
message: jestError.message,
|
||||
stack: jestError.stack,
|
||||
};
|
||||
|
||||
step?.complete({ error: serializedError });
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(serializedError, false /* isHardError */);
|
||||
|
|
|
@ -48,5 +48,5 @@ export async function toBeTruthy(
|
|||
return matches ? `${header}Expected: not ${expected}\nReceived: ${expected}${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
|
||||
// could access them, for example in order to display a custom visual diff,
|
||||
// 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 {
|
||||
locator: receiver,
|
||||
name: matcherName,
|
||||
expected,
|
||||
message,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
@ -5083,38 +5083,81 @@ type AllMatchers<R, T> = PageAssertions & LocatorAssertions & APIResponseAsserti
|
|||
|
||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||
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.
|
||||
*/
|
||||
not: MakeMatchers<R, T>;
|
||||
not: MakeMatchers<R, T, ExtendedMatchers>;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
* If the promise is fulfilled the assertion fails.
|
||||
*/
|
||||
rejects: MakeMatchers<Promise<R>, any>;
|
||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
||||
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
|
||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>;
|
||||
|
||||
export type Expect = {
|
||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
||||
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
|
||||
export type Expect<ExtendedMatchers> = {
|
||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>;
|
||||
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> & {
|
||||
/**
|
||||
* If you know how to test something, `.not` lets you test its opposite.
|
||||
*/
|
||||
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: {
|
||||
message?: string,
|
||||
timeout?: number,
|
||||
soft?: boolean,
|
||||
}) => Expect;
|
||||
}) => Expect<ExtendedMatchers>;
|
||||
getState(): {
|
||||
expand?: boolean;
|
||||
isNot?: boolean;
|
||||
|
@ -5141,7 +5184,7 @@ export const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions, Playwrig
|
|||
export default test;
|
||||
|
||||
export const _baseTest: TestType<{}, {}>;
|
||||
export const expect: Expect;
|
||||
export const expect: Expect<{}>;
|
||||
|
||||
/**
|
||||
* Defines Playwright config
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference path="./expect.d.ts" />
|
||||
|
||||
import { _baseTest as _test, expect as _expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
@ -32,7 +29,7 @@ export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os
|
|||
|
||||
const debug = debugLogger('itest');
|
||||
|
||||
_expect.extend({
|
||||
const expect = _expect.extend({
|
||||
toHaveLoggedSoftwareDownload(received: any, browsers: ('chromium' | 'firefox' | 'webkit' | 'ffmpeg')[]) {
|
||||
if (typeof received !== 'string')
|
||||
throw new Error(`Expected argument to be a string.`);
|
||||
|
@ -42,8 +39,12 @@ _expect.extend({
|
|||
downloaded.add(browser.toLowerCase());
|
||||
|
||||
const expected = browsers;
|
||||
if (expected.length === downloaded.size && expected.every(browser => downloaded.has(browser)))
|
||||
return { pass: true };
|
||||
if (expected.length === downloaded.size && expected.every(browser => downloaded.has(browser))) {
|
||||
return {
|
||||
pass: true,
|
||||
message: () => 'Expected not to download browsers, but did.'
|
||||
};
|
||||
}
|
||||
return {
|
||||
pass: false,
|
||||
message: () => [
|
||||
|
@ -55,8 +56,6 @@ _expect.extend({
|
|||
}
|
||||
});
|
||||
|
||||
const expect = _expect;
|
||||
|
||||
type ExecOptions = { cwd?: string, env?: Record<string, string>, message?: string, expectToExitWithError?: boolean };
|
||||
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
|
||||
|
||||
|
@ -199,7 +198,7 @@ export const test = _test
|
|||
},
|
||||
tsc: async ({ exec }, use) => {
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'Text content',
|
||||
expected: /Text2/,
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'Text content',
|
||||
expected: /Text/,
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'hidden',
|
||||
expected: 'visible',
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'visible',
|
||||
expected: 'visible',
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 0,
|
||||
expected: 1,
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 1,
|
||||
expected: 1,
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'unchecked',
|
||||
expected: 'checked',
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'checked',
|
||||
expected: 'checked',
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'checked',
|
||||
expected: 'unchecked',
|
||||
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);
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
locator: expect.any(Object),
|
||||
actual: 'unchecked',
|
||||
expected: 'unchecked',
|
||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false })`),
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
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 }) => {
|
||||
const result = await runInlineTest({
|
||||
|
@ -273,7 +273,12 @@ test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
|
|||
'a.spec.ts': `
|
||||
import { test, expect, type Page, type APIResponse } from '@playwright/test';
|
||||
test.expect.extend({
|
||||
toBeWithinRange() { },
|
||||
toBeWithinRange() {
|
||||
return {
|
||||
pass: true,
|
||||
message: () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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.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',
|
||||
'PlaywrightWorkerArgs.playwright',
|
||||
'Matchers',
|
||||
'ExpectMatcherUtils',
|
||||
]),
|
||||
doNotExportClassNames: new Set([...assertionClasses, 'TestProject']),
|
||||
includeExperimental,
|
||||
|
|
|
@ -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;
|
||||
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||
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>;
|
||||
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 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.
|
||||
*/
|
||||
not: MakeMatchers<R, T>;
|
||||
not: MakeMatchers<R, T, ExtendedMatchers>;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
* If the promise is fulfilled the assertion fails.
|
||||
*/
|
||||
rejects: MakeMatchers<Promise<R>, any>;
|
||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T>>;
|
||||
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
|
||||
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>;
|
||||
|
||||
export type Expect = {
|
||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
||||
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
|
||||
export type Expect<ExtendedMatchers> = {
|
||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>;
|
||||
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> & {
|
||||
/**
|
||||
* If you know how to test something, `.not` lets you test its opposite.
|
||||
*/
|
||||
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: {
|
||||
message?: string,
|
||||
timeout?: number,
|
||||
soft?: boolean,
|
||||
}) => Expect;
|
||||
}) => Expect<ExtendedMatchers>;
|
||||
getState(): {
|
||||
expand?: boolean;
|
||||
isNot?: boolean;
|
||||
|
@ -399,7 +442,7 @@ export const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions, Playwrig
|
|||
export default test;
|
||||
|
||||
export const _baseTest: TestType<{}, {}>;
|
||||
export const expect: Expect;
|
||||
export const expect: Expect<{}>;
|
||||
|
||||
/**
|
||||
* Defines Playwright config
|
||||
|
|
Загрузка…
Ссылка в новой задаче