cherry-pick(#27555): chore: composed->merge

This commit is contained in:
Pavel Feldman 2023-10-11 13:56:27 -07:00
parent ae31f58b43
commit 3049d99bc8
13 changed files with 71 additions and 49 deletions

View file

@ -8,46 +8,68 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.39 ## Version 1.39
### Extending expect with custom matchers ### Add custom matchers to your expect
You can extend Playwright assertions by providing custom matchers. These matchers will be available on the expect object. You can extend Playwright assertions by providing custom matchers. These matchers will be available on the expect object.
```js title=fixtures.ts ```js title="test.spec.ts"
import { expect as baseExpect } from '@playwright/test'; import { expect as baseExpect } from '@playwright/test';
export const expect = baseExpect.extend({ export const expect = baseExpect.extend({
async toHaveAmount(locator: Locator, expected: number, options?: { timeout?: number }) { async toHaveAmount(locator: Locator, expected: number, options?: { timeout?: number }) {
// Note: this matcher never passes, see the documentation for a full example. // ... see documentation for how to write matchers.
// Return a "pass" flag and a message getter.
return { pass: false, message: () => `Expected ${expected} amount` };
}, },
}); });
test('pass', async ({ page }) => {
await expect(page.getByTestId('cart')).toHaveAmount(5);
});
``` ```
See the documentation [for a full example](./test-configuration.md#add-custom-matchers-using-expectextend). See the documentation [for a full example](./test-configuration.md#add-custom-matchers-using-expectextend).
### Merging fixtures and expect matchers ### Merge test fixtures
You can combine fixtures and custom expect matchers from multiple files or modules. You can now merge test fixtures from multiple files or modules:
```js title="fixtures.ts" ```js title="fixtures.ts"
import { composedTest, composedExpect } from '@playwright/test'; import { mergeTests } from '@playwright/test';
import { test as dbTest } from 'database-test-utils';
import { test as a11yTest } from 'a11y-test-utils';
export const test = mergeTests(dbTest, a11yTest);
```
```js title="test.spec.ts"
import { test } from './fixtures';
test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});
```
### Merge custom expect matchers
You can now merge custom expect matchers from multiple files or modules:
```js title="fixtures.ts"
import { mergeTests, mergeExpects } from '@playwright/test';
import { test as dbTest, expect as dbExpect } from 'database-test-utils'; import { test as dbTest, expect as dbExpect } from 'database-test-utils';
import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils'; import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils';
export const expect = composedExpect(dbExpect, a11yExpect); export const test = mergeTests(dbTest, a11yTest);
export const test = composedTest(dbTest, a11yTest); export const expect = mergeExpects(dbExpect, a11yExpect);
``` ```
```js title="test.spec.ts" ```js title="test.spec.ts"
import { test, expect } from './fixtures'; import { test, expect } from './fixtures';
test('passes', async ({ database, page }) => { test('passes', async ({ page, database }) => {
await expect(database).toHaveDatabaseUser('admin'); await expect(database).toHaveDatabaseUser('admin');
await expect(page).toPassA11yAudit(); await expect(page).toPassA11yAudit();
}); });
``` ```
### Boxed test steps ### Hide implementation details: box test steps
You can mark a [`method: Test.step`] as "boxed" so that errors inside it point to the step call site. You can mark a [`method: Test.step`] as "boxed" so that errors inside it point to the step call site.

View file

@ -217,12 +217,12 @@ Do not confuse Playwright's `expect` with the [`expect` library](https://jestjs.
You can combine custom matchers from multiple files or modules. You can combine custom matchers from multiple files or modules.
```js title="fixtures.ts" ```js title="fixtures.ts"
import { composedTest, composedExpect } from '@playwright/test'; import { mergeTests, mergeExpects } from '@playwright/test';
import { test as dbTest, expect as dbExpect } from 'database-test-utils'; import { test as dbTest, expect as dbExpect } from 'database-test-utils';
import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils'; import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils';
export const expect = composedExpect(dbExpect, a11yExpect); export const expect = mergeExpects(dbExpect, a11yExpect);
export const test = composedTest(dbTest, a11yTest); export const test = mergeTests(dbTest, a11yTest);
``` ```
```js title="test.spec.ts" ```js title="test.spec.ts"

View file

@ -230,7 +230,7 @@ export class TestTypeImpl {
private _extend(location: Location, fixtures: Fixtures) { private _extend(location: Location, fixtures: Fixtures) {
if ((fixtures as any)[testTypeSymbol]) if ((fixtures as any)[testTypeSymbol])
throw new Error(`test.extend() accepts fixtures object, not a test object.\nDid you mean to call composedTest()?`); throw new Error(`test.extend() accepts fixtures object, not a test object.\nDid you mean to call mergeTests()?`);
const fixturesWithLocation: FixturesWithLocation = { fixtures, location }; const fixturesWithLocation: FixturesWithLocation = { fixtures, location };
return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test; return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test;
} }
@ -249,12 +249,12 @@ function throwIfRunningInsideJest() {
export const rootTestType = new TestTypeImpl([]); export const rootTestType = new TestTypeImpl([]);
export function composedTest(...tests: TestType<any, any>[]) { export function mergeTests(...tests: TestType<any, any>[]) {
let result = rootTestType; let result = rootTestType;
for (const t of tests) { for (const t of tests) {
const testTypeImpl = (t as any)[testTypeSymbol] as TestTypeImpl; const testTypeImpl = (t as any)[testTypeSymbol] as TestTypeImpl;
if (!testTypeImpl) if (!testTypeImpl)
throw new Error(`composedTest() accepts "test" functions as parameters.\nDid you mean to call test.extend() with fixtures instead?`); throw new Error(`mergeTests() accepts "test" functions as parameters.\nDid you mean to call test.extend() with fixtures instead?`);
// Filter out common ancestor fixtures. // Filter out common ancestor fixtures.
const newFixtures = testTypeImpl.fixtures.filter(theirs => !result.fixtures.find(ours => ours.fixtures === theirs.fixtures)); const newFixtures = testTypeImpl.fixtures.filter(theirs => !result.fixtures.find(ours => ours.fixtures === theirs.fixtures));
result = new TestTypeImpl([...result.fixtures, ...newFixtures]); result = new TestTypeImpl([...result.fixtures, ...newFixtures]);

View file

@ -744,7 +744,7 @@ function renderApiCall(apiName: string, params: any) {
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures); export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export { defineConfig } from './common/configLoader'; export { defineConfig } from './common/configLoader';
export { composedTest } from './common/testType'; export { mergeTests } from './common/testType';
export { composedExpect } from './matchers/expect'; export { mergeExpects } from './matchers/expect';
export default test; export default test;

View file

@ -338,6 +338,6 @@ function computeArgsSuffix(matcherName: string, args: any[]) {
expectLibrary.extend(customMatchers); expectLibrary.extend(customMatchers);
export function composedExpect(...expects: any[]) { export function mergeExpects(...expects: any[]) {
return expect; return expect;
} }

View file

@ -5328,7 +5328,7 @@ type MergedTestType<List> = TestType<MergedT<List>, MergedW<List>>;
/** /**
* Merges fixtures * Merges fixtures
*/ */
export function composedTest<List extends any[]>(...tests: List): MergedTestType<List>; export function mergeTests<List extends any[]>(...tests: List): MergedTestType<List>;
type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {}; type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {};
type MergedExpect<List> = Expect<MergedExpectMatchers<List>>; type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
@ -5336,7 +5336,7 @@ type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
/** /**
* Merges expects * Merges expects
*/ */
export function composedExpect<List extends any[]>(...expects: List): MergedExpect<List>; export function mergeExpects<List extends any[]>(...expects: List): MergedExpect<List>;
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {}; export {};

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { composedTest } from '@playwright/test'; import { mergeTests } from '@playwright/test';
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import type { CommonFixtures, CommonWorkerFixtures } from './commonFixtures'; import type { CommonFixtures, CommonWorkerFixtures } from './commonFixtures';
import { commonFixtures } from './commonFixtures'; import { commonFixtures } from './commonFixtures';
@ -26,7 +26,7 @@ import { testModeTest } from './testModeFixtures';
export const base = test; export const base = test;
export const baseTest = composedTest(base, coverageTest, platformTest, testModeTest) export const baseTest = mergeTests(base, coverageTest, platformTest, testModeTest)
.extend<CommonFixtures, CommonWorkerFixtures>(commonFixtures) .extend<CommonFixtures, CommonWorkerFixtures>(commonFixtures)
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures); .extend<ServerFixtures, ServerWorkerOptions>(serverFixtures);

View file

@ -1,9 +1,9 @@
import { test as test1, expect as expect1, composedTest, composedExpect } from '@playwright/test'; import { test as test1, expect as expect1, mergeTests, mergeExpects } from '@playwright/test';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
import { test as test2, expect as expect2 } from 'playwright-test-plugin'; import { test as test2, expect as expect2 } from 'playwright-test-plugin';
const test = composedTest(test1, test2); const test = mergeTests(test1, test2);
const expect = composedExpect(expect1, expect2); const expect = mergeExpects(expect1, expect2);
test('sample test', async ({ page, plugin }) => { test('sample test', async ({ page, plugin }) => {
type IsPage = (typeof page) extends Page ? true : never; type IsPage = (typeof page) extends Page ? true : never;

View file

@ -1,8 +1,8 @@
import { test as test1, expect as expect1, composedTest, composedExpect } from '@playwright/test'; import { test as test1, expect as expect1, mergeTests, mergeExpects } from '@playwright/test';
import { test as test2, expect as expect2 } from 'playwright-test-plugin'; import { test as test2, expect as expect2 } from 'playwright-test-plugin';
const test = composedTest(test1, test2); const test = mergeTests(test1, test2);
const expect = composedExpect(expect1, expect2); const expect = mergeExpects(expect1, expect2);
test('sample test', async ({ page, plugin }) => { test('sample test', async ({ page, plugin }) => {
await page.setContent(`<div>hello</div><span>world</span>`); await page.setContent(`<div>hello</div><span>world</span>`);

View file

@ -879,10 +879,10 @@ test('should suppport toHaveAttribute without optional value', async ({ runTSC }
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should support composedExpect (TSC)', async ({ runTSC }) => { test('should support mergeExpects (TSC)', async ({ runTSC }) => {
const result = await runTSC({ const result = await runTSC({
'a.spec.ts': ` 'a.spec.ts': `
import { test, composedExpect, expect as baseExpect } from '@playwright/test'; import { test, mergeExpects, expect as baseExpect } from '@playwright/test';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
const expect1 = baseExpect.extend({ const expect1 = baseExpect.extend({
@ -897,7 +897,7 @@ test('should support composedExpect (TSC)', async ({ runTSC }) => {
} }
}); });
const expect = composedExpect(expect1, expect2); const expect = mergeExpects(expect1, expect2);
test('custom matchers', async ({ page }) => { test('custom matchers', async ({ page }) => {
await expect(page).toBeAGoodPage(123); await expect(page).toBeAGoodPage(123);
@ -914,10 +914,10 @@ test('should support composedExpect (TSC)', async ({ runTSC }) => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should support composedExpect', async ({ runInlineTest }) => { test('should support mergeExpects', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.spec.ts': ` 'a.spec.ts': `
import { test, composedExpect, expect as baseExpect } from '@playwright/test'; import { test, mergeExpects, expect as baseExpect } from '@playwright/test';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
const expect1 = baseExpect.extend({ const expect1 = baseExpect.extend({
@ -932,7 +932,7 @@ test('should support composedExpect', async ({ runInlineTest }) => {
} }
}); });
const expect = composedExpect(expect1, expect2); const expect = mergeExpects(expect1, expect2);
test('custom matchers', async ({ page }) => { test('custom matchers', async ({ page }) => {
await expect(page).toBeAGoodPage(123); await expect(page).toBeAGoodPage(123);

View file

@ -160,7 +160,7 @@ test('config should override options but not fixtures', async ({ runInlineTest }
expect(result.output).toContain('fixture-config-fixture'); expect(result.output).toContain('fixture-config-fixture');
}); });
test('composedTest should be able to merge', async ({ runInlineTest }) => { test('mergeTests should be able to merge', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
@ -168,7 +168,7 @@ test('composedTest should be able to merge', async ({ runInlineTest }) => {
}; };
`, `,
'a.test.ts': ` 'a.test.ts': `
import { test, expect, composedTest } from '@playwright/test'; import { test, expect, mergeTests } from '@playwright/test';
const base = test.extend({ const base = test.extend({
myFixture: 'abc', myFixture: 'abc',
}); });
@ -184,7 +184,7 @@ test('composedTest should be able to merge', async ({ runInlineTest }) => {
fixture2: ({}, use) => use('fixture2'), fixture2: ({}, use) => use('fixture2'),
}); });
const test3 = composedTest(test1, test2); const test3 = mergeTests(test1, test2);
test3('merged', async ({ param, fixture1, myFixture, fixture2 }) => { test3('merged', async ({ param, fixture1, myFixture, fixture2 }) => {
console.log('param-' + param); console.log('param-' + param);
@ -202,7 +202,7 @@ test('composedTest should be able to merge', async ({ runInlineTest }) => {
expect(result.output).toContain('fixture2-fixture2'); expect(result.output).toContain('fixture2-fixture2');
}); });
test('test.extend should print nice message when used as composedTest', async ({ runInlineTest }) => { test('test.extend should print nice message when used as mergeTests', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `
import { test as base, expect } from '@playwright/test'; import { test as base, expect } from '@playwright/test';
@ -215,14 +215,14 @@ test('test.extend should print nice message when used as composedTest', async ({
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.output).toContain('Did you mean to call composedTest()?'); expect(result.output).toContain('Did you mean to call mergeTests()?');
}); });
test('composedTest should print nice message when used as extend', async ({ runInlineTest }) => { test('mergeTests should print nice message when used as extend', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `
import { test as base, expect, composedTest } from '@playwright/test'; import { test as base, expect, mergeTests } from '@playwright/test';
const test3 = composedTest(base, {}); const test3 = mergeTests(base, {});
test3('test', () => {}); test3('test', () => {});
`, `,
}); });

View file

@ -84,7 +84,7 @@ test('can return anything from hooks', async ({ runTSC }) => {
test('test.extend options should check types', async ({ runTSC }) => { test('test.extend options should check types', async ({ runTSC }) => {
const result = await runTSC({ const result = await runTSC({
'helper.ts': ` 'helper.ts': `
import { test as base, expect, composedTest } from '@playwright/test'; import { test as base, expect, mergeTests } from '@playwright/test';
export type Params = { foo: string }; export type Params = { foo: string };
export const test = base; export const test = base;
export const test1 = test.extend<Params>({ foo: [ 'foo', { option: true } ] }); export const test1 = test.extend<Params>({ foo: [ 'foo', { option: true } ] });
@ -100,7 +100,7 @@ test('test.extend options should check types', async ({ runTSC }) => {
// @ts-expect-error // @ts-expect-error
bar: async ({ baz }, run) => { await run(42); } bar: async ({ baz }, run) => { await run(42); }
}); });
export const test4 = composedTest(test1, testW); export const test4 = mergeTests(test1, testW);
const test5 = test4.extend<{}, { hey: string, hey2: string }>({ const test5 = test4.extend<{}, { hey: string, hey2: string }>({
// @ts-expect-error // @ts-expect-error
hey: [async ({ foo }, use) => { hey: [async ({ foo }, use) => {

View file

@ -462,7 +462,7 @@ type MergedTestType<List> = TestType<MergedT<List>, MergedW<List>>;
/** /**
* Merges fixtures * Merges fixtures
*/ */
export function composedTest<List extends any[]>(...tests: List): MergedTestType<List>; export function mergeTests<List extends any[]>(...tests: List): MergedTestType<List>;
type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {}; type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {};
type MergedExpect<List> = Expect<MergedExpectMatchers<List>>; type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
@ -470,7 +470,7 @@ type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
/** /**
* Merges expects * Merges expects
*/ */
export function composedExpect<List extends any[]>(...expects: List): MergedExpect<List>; export function mergeExpects<List extends any[]>(...expects: List): MergedExpect<List>;
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {}; export {};