From daba77644c8d5b50599beb93d5e54f0a1766075c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 4 Oct 2023 15:01:25 -0700 Subject: [PATCH] feat: composedExpect (#27432) Allows to merge multiple expects with custom matchers added by `expect.extend()`. --- packages/playwright/src/index.ts | 1 + packages/playwright/src/matchers/expect.ts | 4 ++ packages/playwright/types/test.d.ts | 8 +++ .../playwright-test-plugin-types.ts | 10 ++- .../fixture-scripts/plugin.spec.ts | 8 ++- .../playwright-test-plugin/index.ts | 30 ++++++++- tests/playwright-test/expect.spec.ts | 67 ++++++++++++++++++- utils/generate_types/overrides-test.d.ts | 8 +++ 8 files changed, 129 insertions(+), 7 deletions(-) diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index d1b1f8b35d..2a02f52315 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -766,5 +766,6 @@ export const test = _baseTest.extend(playwrightFix export { defineConfig } from './common/configLoader'; export { composedTest } from './common/testType'; +export { composedExpect } from './matchers/expect'; export default test; diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 2f916225f0..dc05f09735 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -337,3 +337,7 @@ function computeArgsSuffix(matcherName: string, args: any[]) { } expectLibrary.extend(customMatchers); + +export function composedExpect(...expects: any[]) { + return expect; +} diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 13bc565935..7944d2349a 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5207,6 +5207,14 @@ type MergedTestType = TestType, MergedW>; */ export function composedTest(...tests: List): MergedTestType; +type MergedExpectMatchers = List extends [Expect, ...(infer Rest)] ? M & MergedExpectMatchers : {}; +type MergedExpect = Expect>; + +/** + * Merges expects + */ +export function composedExpect(...expects: List): MergedExpect; + // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/tests/installation/fixture-scripts/playwright-test-plugin-types.ts b/tests/installation/fixture-scripts/playwright-test-plugin-types.ts index 1b8057af5b..e16d612b87 100644 --- a/tests/installation/fixture-scripts/playwright-test-plugin-types.ts +++ b/tests/installation/fixture-scripts/playwright-test-plugin-types.ts @@ -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('
hello world
'); + await expect(page).toContainText('hello'); + // @ts-expect-error + await expect(page).toContainText(123); }); diff --git a/tests/installation/fixture-scripts/plugin.spec.ts b/tests/installation/fixture-scripts/plugin.spec.ts index 23b0bfddca..93a40a7584 100644 --- a/tests/installation/fixture-scripts/plugin.spec.ts +++ b/tests/installation/fixture-scripts/plugin.spec.ts @@ -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(`
hello
world`); @@ -9,4 +10,7 @@ test('sample test', async ({ page, plugin }) => { console.log(`plugin value: ${plugin}`); expect(plugin).toBe('hello from plugin'); + + await page.setContent('
hello world
'); + await expect(page).toContainText('hello'); }); diff --git a/tests/installation/playwright-test-plugin/index.ts b/tests/installation/playwright-test-plugin/index.ts index 8448dce151..258a722f9b 100644 --- a/tests/installation/playwright-test-plugin/index.ts +++ b/tests/installation/playwright-test-plugin/index.ts @@ -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, + }; + } +}); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index ec7ae3c4de..29d38d7e1e 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -878,4 +878,69 @@ test('should suppport toHaveAttribute without optional value', async ({ runTSC } ` }); expect(result.exitCode).toBe(0); -}); \ No newline at end of file +}); + +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); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index b8c4c7e72b..988df1b9bf 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -464,5 +464,13 @@ type MergedTestType = TestType, MergedW>; */ export function composedTest(...tests: List): MergedTestType; +type MergedExpectMatchers = List extends [Expect, ...(infer Rest)] ? M & MergedExpectMatchers : {}; +type MergedExpect = Expect>; + +/** + * Merges expects + */ +export function composedExpect(...expects: List): MergedExpect; + // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {};