fix(expect): proper return types (#13334)
A few changes: - `Matchers<R, T>` now carries both return and argument type. - Based on the argument type, we apply playwright-specific Page/Locator matchers. - Return type is usually void, unless wrapped with `expect.resolves`, `expect.rejects` or `expect.poll()`. - To preserve compatibility with any extended types in the wild, argument type is optional.
This commit is contained in:
parent
6ca58e18cb
commit
4bb563b015
|
|
@ -563,7 +563,7 @@ For TypeScript, also add the following to `global.d.ts`. You don't need it for J
|
||||||
// global.d.ts
|
// global.d.ts
|
||||||
declare global {
|
declare global {
|
||||||
namespace PlaywrightTest {
|
namespace PlaywrightTest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R, T> {
|
||||||
toBeWithinRange(a: number, b: number): R;
|
toBeWithinRange(a: number, b: number): R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
packages/playwright-test/types/testExpect.d.ts
vendored
58
packages/playwright-test/types/testExpect.d.ts
vendored
|
|
@ -22,15 +22,15 @@ export declare type AsymmetricMatcher = Record<string, any>;
|
||||||
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
|
||||||
type ExtraMatchers<T, Type, Matchers> = T extends Type ? Matchers : IfAny<T, Matchers, {}>;
|
type ExtraMatchers<T, Type, Matchers> = T extends Type ? Matchers : IfAny<T, Matchers, {}>;
|
||||||
|
|
||||||
type MakeMatchers<T, ReturnValue = T> = PlaywrightTest.Matchers<ReturnValue> &
|
type MakeMatchers<R, T> = PlaywrightTest.Matchers<R, T> &
|
||||||
ExtraMatchers<T, Page, PageMatchers> &
|
ExtraMatchers<T, Page, PageMatchers> &
|
||||||
ExtraMatchers<T, Locator, LocatorMatchers> &
|
ExtraMatchers<T, Locator, LocatorMatchers> &
|
||||||
ExtraMatchers<T, APIResponse, APIResponseMatchers>
|
ExtraMatchers<T, APIResponse, APIResponseMatchers>;
|
||||||
|
|
||||||
export declare type Expect = {
|
export declare type Expect = {
|
||||||
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<T>;
|
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
|
||||||
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<T>;
|
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
|
||||||
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number }) => Omit<PlaywrightTest.Matchers<T>, 'rejects' | 'resolves'>;
|
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number }) => Omit<PlaywrightTest.Matchers<Promise<void>, T>, 'rejects' | 'resolves'>;
|
||||||
|
|
||||||
extend(arg0: any): void;
|
extend(arg0: any): void;
|
||||||
getState(): expect.MatcherState;
|
getState(): expect.MatcherState;
|
||||||
|
|
@ -109,21 +109,21 @@ type SupportedExpectProperties =
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
export namespace PlaywrightTest {
|
export namespace PlaywrightTest {
|
||||||
export interface Matchers<R> extends Pick<expect.Matchers<R>, SupportedExpectProperties> {
|
export interface Matchers<R, T = unknown> extends Pick<expect.Matchers<R>, SupportedExpectProperties> {
|
||||||
/**
|
/**
|
||||||
* If you know how to test something, `.not` lets you test its opposite.
|
* If you know how to test something, `.not` lets you test its opposite.
|
||||||
*/
|
*/
|
||||||
not: MakeMatchers<R>;
|
not: MakeMatchers<R, T>;
|
||||||
/**
|
/**
|
||||||
* Use resolves to unwrap the value of a fulfilled promise so any other
|
* 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.
|
* matcher can be chained. If the promise is rejected the assertion fails.
|
||||||
*/
|
*/
|
||||||
resolves: MakeMatchers<Awaited<R>, R>;
|
resolves: MakeMatchers<Promise<R>, Awaited<T>>;
|
||||||
/**
|
/**
|
||||||
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
* Unwraps the reason of a rejected promise so any other matcher can be chained.
|
||||||
* If the promise is fulfilled the assertion fails.
|
* If the promise is fulfilled the assertion fails.
|
||||||
*/
|
*/
|
||||||
rejects: MakeMatchers<Promise<R>>;
|
rejects: MakeMatchers<Promise<R>, Awaited<T>>;
|
||||||
/**
|
/**
|
||||||
* Match snapshot
|
* Match snapshot
|
||||||
*/
|
*/
|
||||||
|
|
@ -142,105 +142,105 @@ interface LocatorMatchers {
|
||||||
/**
|
/**
|
||||||
* Asserts input is checked (or unchecked if { checked: false } is passed).
|
* Asserts input is checked (or unchecked if { checked: false } is passed).
|
||||||
*/
|
*/
|
||||||
toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise<Locator>;
|
toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts input is disabled.
|
* Asserts input is disabled.
|
||||||
*/
|
*/
|
||||||
toBeDisabled(options?: { timeout?: number }): Promise<Locator>;
|
toBeDisabled(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts input is editable.
|
* Asserts input is editable.
|
||||||
*/
|
*/
|
||||||
toBeEditable(options?: { timeout?: number }): Promise<Locator>;
|
toBeEditable(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts given DOM node or input has no text content or no input value.
|
* Asserts given DOM node or input has no text content or no input value.
|
||||||
*/
|
*/
|
||||||
toBeEmpty(options?: { timeout?: number }): Promise<Locator>;
|
toBeEmpty(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts input is enabled.
|
* Asserts input is enabled.
|
||||||
*/
|
*/
|
||||||
toBeEnabled(options?: { timeout?: number }): Promise<Locator>;
|
toBeEnabled(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts given DOM is a focused (active) in document.
|
* Asserts given DOM is a focused (active) in document.
|
||||||
*/
|
*/
|
||||||
toBeFocused(options?: { timeout?: number }): Promise<Locator>;
|
toBeFocused(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts given DOM node is hidden or detached from DOM.
|
* Asserts given DOM node is hidden or detached from DOM.
|
||||||
*/
|
*/
|
||||||
toBeHidden(options?: { timeout?: number }): Promise<Locator>;
|
toBeHidden(options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts element's text content matches given pattern or contains given substring.
|
* Asserts element's text content matches given pattern or contains given substring.
|
||||||
*/
|
*/
|
||||||
toContainText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<Locator>;
|
toContainText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts element's attributes `name` matches expected value.
|
* Asserts element's attributes `name` matches expected value.
|
||||||
*/
|
*/
|
||||||
toHaveAttribute(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
|
toHaveAttribute(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that DOM node has a given CSS class.
|
* Asserts that DOM node has a given CSS class.
|
||||||
*/
|
*/
|
||||||
toHaveClass(className: string | RegExp | (string | RegExp)[], options?: { timeout?: number }): Promise<Locator>;
|
toHaveClass(className: string | RegExp | (string | RegExp)[], options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts number of DOM nodes matching given locator.
|
* Asserts number of DOM nodes matching given locator.
|
||||||
*/
|
*/
|
||||||
toHaveCount(expected: number, options?: { timeout?: number }): Promise<Locator>;
|
toHaveCount(expected: number, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts element's computed CSS property `name` matches expected value.
|
* Asserts element's computed CSS property `name` matches expected value.
|
||||||
*/
|
*/
|
||||||
toHaveCSS(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
|
toHaveCSS(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts element's `id` attribute matches expected value.
|
* Asserts element's `id` attribute matches expected value.
|
||||||
*/
|
*/
|
||||||
toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
|
toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts JavaScript object that corresponds to the Node has a property with given value.
|
* Asserts JavaScript object that corresponds to the Node has a property with given value.
|
||||||
*/
|
*/
|
||||||
toHaveJSProperty(name: string, value: any, options?: { timeout?: number }): Promise<Locator>;
|
toHaveJSProperty(name: string, value: any, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts element's text content.
|
* Asserts element's text content.
|
||||||
*/
|
*/
|
||||||
toHaveText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<Locator>;
|
toHaveText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts input element's value.
|
* Asserts input element's value.
|
||||||
*/
|
*/
|
||||||
toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
|
toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts given DOM node visible on the screen.
|
* Asserts given DOM node visible on the screen.
|
||||||
*/
|
*/
|
||||||
toBeVisible(options?: { timeout?: number }): Promise<Locator>;
|
toBeVisible(options?: { timeout?: number }): Promise<void>;
|
||||||
}
|
}
|
||||||
interface PageMatchers {
|
interface PageMatchers {
|
||||||
/**
|
/**
|
||||||
* Asserts page's title.
|
* Asserts page's title.
|
||||||
*/
|
*/
|
||||||
toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
|
toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts page's URL.
|
* Asserts page's URL.
|
||||||
*/
|
*/
|
||||||
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
|
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIResponseMatchers {
|
interface APIResponseMatchers {
|
||||||
/**
|
/**
|
||||||
* Asserts given APIResponse's status is between 200 and 299.
|
* Asserts given APIResponse's status is between 200 and 299.
|
||||||
*/
|
*/
|
||||||
toBeOK(): Promise<APIResponse>;
|
toBeOK(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { };
|
export { };
|
||||||
|
|
|
||||||
|
|
@ -179,12 +179,13 @@ test('should work with default expect matchers and esModuleInterop=false', async
|
||||||
test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
|
test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
|
||||||
const result = await runTSC({
|
const result = await runTSC({
|
||||||
'global.d.ts': `
|
'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 {
|
declare namespace PlaywrightTest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
toBeEmpty(): R;
|
toBeEmpty(): R;
|
||||||
}
|
}
|
||||||
|
interface Matchers<R, T> {
|
||||||
|
toBeNonEmpty(): R;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
@ -199,6 +200,7 @@ test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
|
||||||
test.expect(['hello']).not.toBeEmpty();
|
test.expect(['hello']).not.toBeEmpty();
|
||||||
test.expect({}).toBeEmpty();
|
test.expect({}).toBeEmpty();
|
||||||
test.expect({ hello: 'world' }).not.toBeEmpty();
|
test.expect({ hello: 'world' }).not.toBeEmpty();
|
||||||
|
test.expect('').toBeNonEmpty();
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
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);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return void/Promise when appropriate', async ({ runTSC }) => {
|
||||||
|
const result = await runTSC({
|
||||||
|
'a.spec.ts': `
|
||||||
|
type AssertType<T, S> = S extends T ? AssertNotAny<S> : false;
|
||||||
|
type AssertNotAny<S> = {notRealProperty: number} extends S ? false : true;
|
||||||
|
|
||||||
|
pwt.test('example', async ({ page }) => {
|
||||||
|
{
|
||||||
|
const value = expect(1).toBe(2);
|
||||||
|
const assertion: AssertType<void, typeof value> = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const value = expect(1).not.toBe(2);
|
||||||
|
const assertion: AssertType<void, typeof value> = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const value = expect(page).toHaveURL('');
|
||||||
|
const assertion: AssertType<Promise<void>, typeof value> = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const value = expect(Promise.resolve(1)).resolves.toBe(1);
|
||||||
|
const assertion: AssertType<Promise<void>, typeof value> = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const value = expect.soft(1).toBe(2);
|
||||||
|
const assertion: AssertType<void, typeof value> = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const value = expect.poll(() => true).toBe(2);
|
||||||
|
const assertion: AssertType<Promise<void>, typeof value> = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('helpful expect errors', () => {
|
test.describe('helpful expect errors', () => {
|
||||||
test('top-level', async ({ runInlineTest }) => {
|
test('top-level', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue