feat: composedExpect (#27432)

Allows to merge multiple expects with custom matchers added by
`expect.extend()`.
This commit is contained in:
Dmitry Gozman 2023-10-04 15:01:25 -07:00 committed by GitHub
parent d426f2fd4e
commit daba77644c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 7 deletions

View File

@ -766,5 +766,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFix
export { defineConfig } from './common/configLoader';
export { composedTest } from './common/testType';
export { composedExpect } from './matchers/expect';
export default test;

View File

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

View File

@ -5207,6 +5207,14 @@ type MergedTestType<List> = TestType<MergedT<List>, MergedW<List>>;
*/
export function composedTest<List extends any[]>(...tests: List): MergedTestType<List>;
type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {};
type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
/**
* Merges expects
*/
export function composedExpect<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
export {};

View File

@ -1,8 +1,9 @@
import { test as test1, composedTest } from '@playwright/test';
import { test as test1, expect as expect1, composedTest, composedExpect } from '@playwright/test';
import type { Page } from '@playwright/test';
import { test as test2 } from 'playwright-test-plugin';
import { test as test2, expect as expect2 } from 'playwright-test-plugin';
const test = composedTest(test1, test2);
const expect = composedExpect(expect1, expect2);
test('sample test', async ({ page, plugin }) => {
type IsPage = (typeof page) extends Page ? true : never;
@ -10,4 +11,9 @@ test('sample test', async ({ page, plugin }) => {
type IsString = (typeof plugin) extends string ? true : never;
const isString: IsString = true;
await page.setContent('<div>hello world</div>');
await expect(page).toContainText('hello');
// @ts-expect-error
await expect(page).toContainText(123);
});

View File

@ -1,7 +1,8 @@
import { test as test1, expect, composedTest } from '@playwright/test';
import { test as test2 } from 'playwright-test-plugin';
import { test as test1, expect as expect1, composedTest, composedExpect } from '@playwright/test';
import { test as test2, expect as expect2 } from 'playwright-test-plugin';
const test = composedTest(test1, test2);
const expect = composedExpect(expect1, expect2);
test('sample test', async ({ page, plugin }) => {
await page.setContent(`<div>hello</div><span>world</span>`);
@ -9,4 +10,7 @@ test('sample test', async ({ page, plugin }) => {
console.log(`plugin value: ${plugin}`);
expect(plugin).toBe('hello from plugin');
await page.setContent('<div>hello world</div>');
await expect(page).toContainText('hello');
});

View File

@ -14,10 +14,36 @@
* limitations under the License.
*/
import { test as base } from '@playwright/test';
import { test as baseTest, expect as expectBase } from '@playwright/test';
import type { Page } from '@playwright/test';
export const test = base.extend<{ plugin: string }>({
export const test = baseTest.extend<{ plugin: string }>({
plugin: async ({}, use) => {
await use('hello from plugin');
},
});
export const expect = expectBase.extend({
async toContainText(page: Page, expected: string) {
const locator = page.getByText(expected);
let pass: boolean;
let matcherResult: any;
try {
await expectBase(locator).toBeVisible();
pass = true;
} catch (e: any) {
matcherResult = e.matcherResult;
pass = false;
}
return {
name: 'toContainText',
expected,
message: () => matcherResult.message,
pass,
actual: matcherResult?.actual,
log: matcherResult?.log,
};
}
});

View File

@ -878,4 +878,69 @@ test('should suppport toHaveAttribute without optional value', async ({ runTSC }
`
});
expect(result.exitCode).toBe(0);
});
});
test('should support composedExpect (TSC)', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
import { test, composedExpect, expect as baseExpect } from '@playwright/test';
import type { Page } from '@playwright/test';
const expect1 = baseExpect.extend({
async toBeAGoodPage(page: Page, x: number) {
return { pass: true, message: () => '' };
}
});
const expect2 = baseExpect.extend({
async toBeABadPage(page: Page, y: string) {
return { pass: true, message: () => '' };
}
});
const expect = composedExpect(expect1, expect2);
test('custom matchers', async ({ page }) => {
await expect(page).toBeAGoodPage(123);
await expect(page).toBeABadPage('123');
// @ts-expect-error
await expect(page).toBeAMedicorePage();
// @ts-expect-error
await expect(page).toBeABadPage(123);
// @ts-expect-error
await expect(page).toBeAGoodPage('123');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should support composedExpect', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, composedExpect, expect as baseExpect } from '@playwright/test';
import type { Page } from '@playwright/test';
const expect1 = baseExpect.extend({
async toBeAGoodPage(page: Page, x: number) {
return { pass: true, message: () => '' };
}
});
const expect2 = baseExpect.extend({
async toBeABadPage(page: Page, y: string) {
return { pass: true, message: () => '' };
}
});
const expect = composedExpect(expect1, expect2);
test('custom matchers', async ({ page }) => {
await expect(page).toBeAGoodPage(123);
await expect(page).toBeABadPage('123');
});
`
}, { workers: 1 });
expect(result.passed).toBe(1);
expect(result.exitCode).toBe(0);
});

View File

@ -464,5 +464,13 @@ type MergedTestType<List> = TestType<MergedT<List>, MergedW<List>>;
*/
export function composedTest<List extends any[]>(...tests: List): MergedTestType<List>;
type MergedExpectMatchers<List> = List extends [Expect<infer M>, ...(infer Rest)] ? M & MergedExpectMatchers<Rest> : {};
type MergedExpect<List> = Expect<MergedExpectMatchers<List>>;
/**
* Merges expects
*/
export function composedExpect<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
export {};