diff --git a/docs/src/test-advanced-js.md b/docs/src/test-advanced-js.md index 74195d2654..f769940901 100644 --- a/docs/src/test-advanced-js.md +++ b/docs/src/test-advanced-js.md @@ -563,7 +563,7 @@ For TypeScript, also add the following to `global.d.ts`. You don't need it for J // global.d.ts declare global { namespace PlaywrightTest { - interface Matchers { + interface Matchers { toBeWithinRange(a: number, b: number): R; } } diff --git a/packages/playwright-test/types/testExpect.d.ts b/packages/playwright-test/types/testExpect.d.ts index ee19ceff0c..81b7e78626 100644 --- a/packages/playwright-test/types/testExpect.d.ts +++ b/packages/playwright-test/types/testExpect.d.ts @@ -22,15 +22,15 @@ export declare type AsymmetricMatcher = Record; type IfAny = 0 extends (1 & T) ? Y : N; type ExtraMatchers = T extends Type ? Matchers : IfAny; -type MakeMatchers = PlaywrightTest.Matchers & +type MakeMatchers = PlaywrightTest.Matchers & ExtraMatchers & ExtraMatchers & - ExtraMatchers + ExtraMatchers; export declare type Expect = { - (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; - soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number }) => Omit, 'rejects' | 'resolves'>; + (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; + soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number }) => Omit, T>, 'rejects' | 'resolves'>; extend(arg0: any): void; getState(): expect.MatcherState; @@ -109,21 +109,21 @@ type SupportedExpectProperties = declare global { export namespace PlaywrightTest { - export interface Matchers extends Pick, SupportedExpectProperties> { + export interface Matchers extends Pick, SupportedExpectProperties> { /** * If you know how to test something, `.not` lets you test its opposite. */ - not: MakeMatchers; + not: MakeMatchers; /** * 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, R>; + resolves: MakeMatchers, Awaited>; /** * Unwraps the reason of a rejected promise so any other matcher can be chained. * If the promise is fulfilled the assertion fails. */ - rejects: MakeMatchers>; + rejects: MakeMatchers, Awaited>; /** * Match snapshot */ @@ -142,105 +142,105 @@ interface LocatorMatchers { /** * Asserts input is checked (or unchecked if { checked: false } is passed). */ - toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise; + toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise; /** * Asserts input is disabled. */ - toBeDisabled(options?: { timeout?: number }): Promise; + toBeDisabled(options?: { timeout?: number }): Promise; /** * Asserts input is editable. */ - toBeEditable(options?: { timeout?: number }): Promise; + toBeEditable(options?: { timeout?: number }): Promise; /** * Asserts given DOM node or input has no text content or no input value. */ - toBeEmpty(options?: { timeout?: number }): Promise; + toBeEmpty(options?: { timeout?: number }): Promise; /** * Asserts input is enabled. */ - toBeEnabled(options?: { timeout?: number }): Promise; + toBeEnabled(options?: { timeout?: number }): Promise; /** * Asserts given DOM is a focused (active) in document. */ - toBeFocused(options?: { timeout?: number }): Promise; + toBeFocused(options?: { timeout?: number }): Promise; /** * Asserts given DOM node is hidden or detached from DOM. */ - toBeHidden(options?: { timeout?: number }): Promise; + toBeHidden(options?: { timeout?: number }): Promise; /** * Asserts element's text content matches given pattern or contains given substring. */ - toContainText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise; + toContainText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise; /** * Asserts element's attributes `name` matches expected value. */ - toHaveAttribute(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveAttribute(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise; /** * Asserts that DOM node has a given CSS class. */ - toHaveClass(className: string | RegExp | (string | RegExp)[], options?: { timeout?: number }): Promise; + toHaveClass(className: string | RegExp | (string | RegExp)[], options?: { timeout?: number }): Promise; /** * Asserts number of DOM nodes matching given locator. */ - toHaveCount(expected: number, options?: { timeout?: number }): Promise; + toHaveCount(expected: number, options?: { timeout?: number }): Promise; /** * Asserts element's computed CSS property `name` matches expected value. */ - toHaveCSS(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveCSS(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise; /** * Asserts element's `id` attribute matches expected value. */ - toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise; /** * Asserts JavaScript object that corresponds to the Node has a property with given value. */ - toHaveJSProperty(name: string, value: any, options?: { timeout?: number }): Promise; + toHaveJSProperty(name: string, value: any, options?: { timeout?: number }): Promise; /** * Asserts element's text content. */ - toHaveText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise; + toHaveText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise; /** * Asserts input element's value. */ - toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise; /** * Asserts given DOM node visible on the screen. */ - toBeVisible(options?: { timeout?: number }): Promise; + toBeVisible(options?: { timeout?: number }): Promise; } interface PageMatchers { /** * Asserts page's title. */ - toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise; /** * Asserts page's URL. */ - toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise; + toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise; } interface APIResponseMatchers { /** * Asserts given APIResponse's status is between 200 and 299. */ - toBeOK(): Promise; + toBeOK(): Promise; } export { }; diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index b1cb90531c..f5b44e72f2 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -179,12 +179,13 @@ test('should work with default expect matchers and esModuleInterop=false', async test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => { const result = await runTSC({ 'global.d.ts': ` - // Extracted example from their typings. - // Reference: https://github.com/jest-community/jest-extended/blob/master/types/index.d.ts declare namespace PlaywrightTest { interface Matchers { toBeEmpty(): R; } + interface Matchers { + toBeNonEmpty(): R; + } } `, 'a.spec.ts': ` @@ -199,6 +200,7 @@ test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => { test.expect(['hello']).not.toBeEmpty(); test.expect({}).toBeEmpty(); test.expect({ hello: 'world' }).not.toBeEmpty(); + test.expect('').toBeNonEmpty(); ` }); expect(result.exitCode).toBe(0); @@ -234,6 +236,48 @@ test('should propose only the relevant matchers when custom expect matcher class expect(result.exitCode).toBe(0); }); +test('should return void/Promise when appropriate', async ({ runTSC }) => { + const result = await runTSC({ + 'a.spec.ts': ` + type AssertType = S extends T ? AssertNotAny : false; + type AssertNotAny = {notRealProperty: number} extends S ? false : true; + + pwt.test('example', async ({ page }) => { + { + const value = expect(1).toBe(2); + const assertion: AssertType = true; + } + + { + const value = expect(1).not.toBe(2); + const assertion: AssertType = true; + } + + { + const value = expect(page).toHaveURL(''); + const assertion: AssertType, typeof value> = true; + } + + { + const value = expect(Promise.resolve(1)).resolves.toBe(1); + const assertion: AssertType, typeof value> = true; + } + + { + const value = expect.soft(1).toBe(2); + const assertion: AssertType = true; + } + + { + const value = expect.poll(() => true).toBe(2); + const assertion: AssertType, typeof value> = true; + } + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + test.describe('helpful expect errors', () => { test('top-level', async ({ runInlineTest }) => { const result = await runInlineTest({