mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: document chaining expect.extend (#27262)
Fixes https://github.com/microsoft/playwright/issues/15951
This commit is contained in:
parent
a6a0257c88
commit
d6ec1ae399
@ -153,81 +153,57 @@ export default defineConfig({
|
||||
|
||||
You can extend Playwright assertions by providing custom matchers. These matchers will be available on the `expect` object.
|
||||
|
||||
In this example we add a custom `toBeWithinRange` function in the configuration file. Custom matcher should return a `message` callback and a `pass` flag indicating whether the assertion passed.
|
||||
In this example we add a custom `toHaveAmount` function. Custom matcher should return a `message` callback and a `pass` flag indicating whether the assertion passed.
|
||||
|
||||
```js tab=js-js title="playwright.config.ts"
|
||||
const { expect, defineConfig } = require('@playwright/test');
|
||||
```js title="fixtures.ts"
|
||||
import { expect as baseExpect } from '@playwright/test';
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
expect.extend({
|
||||
toBeWithinRange(received, floor, ceiling) {
|
||||
const pass = received >= floor && received <= ceiling;
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => 'passed',
|
||||
pass: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => 'failed',
|
||||
pass: false,
|
||||
};
|
||||
export { test } from '@playwright/test';
|
||||
|
||||
export const expect = baseExpect.extend({
|
||||
async toHaveAmount(locator: Locator, expected: number, options?: { timeout?: number }) {
|
||||
let pass: boolean;
|
||||
let matcherResult: any;
|
||||
try {
|
||||
await baseExpect(locator).toHaveAttribute('data-amount', String(expected), options);
|
||||
pass = true;
|
||||
} catch (e: any) {
|
||||
matcherResult = e.matcherResult;
|
||||
pass = false;
|
||||
}
|
||||
|
||||
const message = pass
|
||||
? () => this.utils.matcherHint('toHaveAmount', locator, expected, { isNot: this.isNot }) +
|
||||
'\n\n' +
|
||||
`Expected: \${this.isNot ? 'not' : ''}\${this.utils.printExpected(expected)}\n` +
|
||||
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
|
||||
: () => this.utils.matcherHint('toHaveAmount', locator, expected, expectOptions) +
|
||||
'\n\n' +
|
||||
`Expected: ${this.utils.printExpected(expected)}\n` +
|
||||
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');
|
||||
|
||||
return {
|
||||
message,
|
||||
pass,
|
||||
name: 'toHaveAmount',
|
||||
expected,
|
||||
actual: matcherResult?.actual,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = defineConfig({});
|
||||
```
|
||||
|
||||
```js tab=js-ts title="playwright.config.ts"
|
||||
import { expect, defineConfig } from '@playwright/test';
|
||||
|
||||
expect.extend({
|
||||
toBeWithinRange(received: number, floor: number, ceiling: number) {
|
||||
const pass = received >= floor && received <= ceiling;
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => 'passed',
|
||||
pass: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => 'failed',
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({});
|
||||
```
|
||||
|
||||
Now we can use `toBeWithinRange` in the test.
|
||||
Now we can use `toHaveAmount` in the test.
|
||||
|
||||
```js title="example.spec.ts"
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('numeric ranges', () => {
|
||||
expect(100).toBeWithinRange(90, 110);
|
||||
expect(101).not.toBeWithinRange(0, 100);
|
||||
test('amount', async () => {
|
||||
await expect(page.locator('.cart')).toHaveAmount(4);
|
||||
});
|
||||
```
|
||||
|
||||
:::note
|
||||
Do not confuse Playwright's `expect` with the [`expect` library](https://jestjs.io/docs/expect). The latter is not fully integrated with Playwright test runner, so make sure to use Playwright's own `expect`.
|
||||
:::
|
||||
|
||||
For TypeScript, also add the following to your [`global.d.ts`](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html). If it does not exist, you need to create it inside your repository. Make sure that your `global.d.ts` gets included inside your `tsconfig.json` via the `files`, `include` or `compilerOptions.typeRoots` option so that your IDE will pick it up.
|
||||
|
||||
You don't need it for JavaScript.
|
||||
|
||||
```js title="global.d.ts"
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace PlaywrightTest {
|
||||
interface Matchers<R, T> {
|
||||
toBeWithinRange(a: number, b: number): R;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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, stringifyStackFrames, trimLongString } from '../util';
|
||||
import { filteredStackTrace, trimLongString } from '../util';
|
||||
import {
|
||||
expect as expectLibrary,
|
||||
INVERTED_COLOR,
|
||||
@ -58,6 +58,7 @@ import {
|
||||
export type { ExpectMatcherContext } from '../common/expectBundle';
|
||||
import { zones } from 'playwright-core/lib/utils';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import { ExpectError } from './matcherHint';
|
||||
|
||||
// #region
|
||||
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
|
||||
@ -263,31 +264,17 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
laxParent: true,
|
||||
}) : undefined;
|
||||
|
||||
const reportStepError = (jestError: Error) => {
|
||||
const message = jestError.message;
|
||||
if (customMessage) {
|
||||
const messageLines = message.split('\n');
|
||||
const newMessage = [
|
||||
customMessage,
|
||||
'',
|
||||
...messageLines,
|
||||
].join('\n');
|
||||
jestError.message = newMessage;
|
||||
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
}
|
||||
|
||||
// Use the exact stack that we entered the matcher with.
|
||||
jestError.stack = jestError.name + ': ' + jestError.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
const reportStepError = (jestError: ExpectError) => {
|
||||
const error = new ExpectError(jestError, customMessage, stackFrames);
|
||||
const serializedError = {
|
||||
message: jestError.message,
|
||||
stack: jestError.stack,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
|
||||
step?.complete({ error: serializedError });
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(serializedError, false /* isHardError */);
|
||||
else
|
||||
throw jestError;
|
||||
throw error;
|
||||
};
|
||||
|
||||
const finalizer = () => {
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import type { ExpectMatcherContext } from './expect';
|
||||
import type { Locator } from 'playwright-core';
|
||||
import { stringifyStackFrames } from '../util';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
export function matcherHint(state: ExpectMatcherContext, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout?: number) {
|
||||
let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + '\n\n';
|
||||
@ -28,11 +30,34 @@ export function matcherHint(state: ExpectMatcherContext, locator: Locator | unde
|
||||
}
|
||||
|
||||
export type MatcherResult<E, A> = {
|
||||
locator?: Locator;
|
||||
name: string;
|
||||
expected: E;
|
||||
message: () => string;
|
||||
pass: boolean;
|
||||
actual?: A;
|
||||
log?: string[];
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
export class ExpectError extends Error {
|
||||
matcherResult: {
|
||||
message: string;
|
||||
pass: boolean;
|
||||
name?: string;
|
||||
expected?: any;
|
||||
actual?: any;
|
||||
log?: string[];
|
||||
timeout?: number;
|
||||
};
|
||||
constructor(jestError: ExpectError, customMessage: string, stackFrames: StackFrame[]) {
|
||||
super('');
|
||||
// Copy to erase the JestMatcherError constructor name from the console.log(error).
|
||||
this.name = jestError.name;
|
||||
this.message = jestError.message;
|
||||
this.matcherResult = jestError.matcherResult;
|
||||
|
||||
if (customMessage)
|
||||
this.message = customMessage + '\n\n' + this.message;
|
||||
this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,5 +48,13 @@ export async function toBeTruthy(
|
||||
return matches ? `${header}Expected: not ${expected}\nReceived: ${expected}${logText}` :
|
||||
`${header}Expected: ${expected}\nReceived: ${unexpected}${logText}`;
|
||||
};
|
||||
return { message, pass: matches, actual, name: matcherName, expected, log };
|
||||
return {
|
||||
message,
|
||||
pass: matches,
|
||||
actual,
|
||||
name: matcherName,
|
||||
expected,
|
||||
log,
|
||||
timeout: timedOut ? timeout : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -67,5 +67,12 @@ 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 { actual: received, expected, message, name: matcherName, pass, log };
|
||||
return {
|
||||
actual: received,
|
||||
expected, message,
|
||||
name: matcherName,
|
||||
pass,
|
||||
log,
|
||||
timeout: timedOut ? timeout : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -160,7 +160,6 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||
createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult {
|
||||
const unfiltered: ImageMatcherResult = {
|
||||
name: this.matcherName,
|
||||
locator: this.locator,
|
||||
expected: this.snapshotPath,
|
||||
actual: this.actualPath,
|
||||
diff: this.diffPath,
|
||||
|
||||
@ -105,6 +105,7 @@ export async function toMatchText(
|
||||
pass,
|
||||
actual: received,
|
||||
log,
|
||||
timeout: timedOut ? timeout : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ test('toMatchText-based assertions should have matcher result', async ({ page })
|
||||
name: 'toHaveText',
|
||||
pass: false,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toHaveText(expected)
|
||||
@ -52,6 +53,7 @@ Call log`);
|
||||
name: 'toHaveText',
|
||||
pass: true,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toHaveText(expected)
|
||||
|
||||
@ -77,6 +79,7 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page })
|
||||
name: 'toBeVisible',
|
||||
pass: false,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeVisible()
|
||||
@ -98,6 +101,7 @@ Call log`);
|
||||
name: 'toBeVisible',
|
||||
pass: true,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeVisible()
|
||||
@ -123,6 +127,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||
name: 'toHaveCount',
|
||||
pass: false,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toHaveCount(expected)
|
||||
@ -143,6 +148,7 @@ Call log`);
|
||||
name: 'toHaveCount',
|
||||
pass: true,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toHaveCount(expected)
|
||||
@ -171,6 +177,7 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag
|
||||
name: 'toBeChecked',
|
||||
pass: false,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeChecked()
|
||||
@ -192,6 +199,7 @@ Call log`);
|
||||
name: 'toBeChecked',
|
||||
pass: true,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeChecked()
|
||||
@ -213,6 +221,7 @@ Call log`);
|
||||
name: 'toBeChecked',
|
||||
pass: false,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeChecked({ checked: false })
|
||||
@ -234,6 +243,7 @@ Call log`);
|
||||
name: 'toBeChecked',
|
||||
pass: true,
|
||||
log: expect.any(Array),
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user