feat(test-runner): specific playwright types for expect (#10670)

This commit is contained in:
Joel Einbinder 2021-12-13 13:42:36 -05:00 committed by GitHub
parent 9f2a040241
commit 7a02c52144
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 131 additions and 115 deletions

View file

@ -15,11 +15,20 @@
*/ */
import type * as expect from 'expect'; import type * as expect from 'expect';
import type { Page, Locator, APIResponse } from 'playwright-core';
export declare type AsymmetricMatcher = Record<string, any>; export declare type AsymmetricMatcher = Record<string, any>;
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
type ExtraMatchers<T, Type, Matchers> = T extends Type ? Matchers : IfAny<T, Matchers, {}>;
type MakeMatchers<T, ReturnValue = T> = PlaywrightTest.Matchers<ReturnValue> &
ExtraMatchers<T, Page, PageMatchers> &
ExtraMatchers<T, Locator, LocatorMatchers> &
ExtraMatchers<T, APIResponse, APIResponseMatchers>
export declare type Expect = { export declare type Expect = {
<T = unknown>(actual: T): PlaywrightTest.Matchers<T>; <T = unknown>(actual: T): MakeMatchers<T>;
// Sourced from node_modules/expect/build/types.d.ts // Sourced from node_modules/expect/build/types.d.ts
assertions(arg0: number): void; assertions(arg0: number): void;
@ -36,6 +45,8 @@ export declare type Expect = {
stringMatching(expected: string | RegExp): AsymmetricMatcher; stringMatching(expected: string | RegExp): AsymmetricMatcher;
}; };
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
type OverriddenExpectProperties = type OverriddenExpectProperties =
'not' | 'not' |
'resolves' | 'resolves' |
@ -51,17 +62,17 @@ declare global {
/** /**
* 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: PlaywrightTest.Matchers<R>; not: MakeMatchers<R>;
/** /**
* 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: PlaywrightTest.Matchers<Promise<R>>; resolves: MakeMatchers<Awaited<R>, R>;
/** /**
* 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: PlaywrightTest.Matchers<Promise<R>>; rejects: MakeMatchers<Promise<R>>;
/** /**
* Match snapshot * Match snapshot
*/ */
@ -75,108 +86,113 @@ declare global {
toMatchSnapshot(name: string | string[], options?: { toMatchSnapshot(name: string | string[], options?: {
threshold?: number threshold?: number
}): R; }): R;
}
}
}
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<R>; toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise<Locator>;
/** /**
* Asserts input is disabled. * Asserts input is disabled.
*/ */
toBeDisabled(options?: { timeout?: number }): Promise<R>; toBeDisabled(options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts input is editable. * Asserts input is editable.
*/ */
toBeEditable(options?: { timeout?: number }): Promise<R>; toBeEditable(options?: { timeout?: number }): Promise<Locator>;
/** /**
* 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<R>; toBeEmpty(options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts input is enabled. * Asserts input is enabled.
*/ */
toBeEnabled(options?: { timeout?: number }): Promise<R>; toBeEnabled(options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts given DOM is a focused (active) in document. * Asserts given DOM is a focused (active) in document.
*/ */
toBeFocused(options?: { timeout?: number }): Promise<R>; toBeFocused(options?: { timeout?: number }): Promise<Locator>;
/** /**
* 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<R>; toBeHidden(options?: { timeout?: number }): Promise<Locator>;
/**
* Asserts given APIResponse's status is between 200 and 299.
*/
toBeOK(): Promise<R>;
/**
* Asserts given DOM node visible on the screen.
*/
toBeVisible(options?: { timeout?: number }): Promise<R>;
/** /**
* 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<R>; toContainText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<Locator>;
/** /**
* 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<R>; toHaveAttribute(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
/** /**
* 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<R>; toHaveClass(className: string | RegExp | (string | RegExp)[], options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts number of DOM nodes matching given locator. * Asserts number of DOM nodes matching given locator.
*/ */
toHaveCount(expected: number, options?: { timeout?: number }): Promise<R>; toHaveCount(expected: number, options?: { timeout?: number }): Promise<Locator>;
/** /**
* 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<R>; toHaveCSS(name: string, expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts element's `id` attribute matches expected value. * Asserts element's `id` attribute matches expected value.
*/ */
toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise<R>; toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
/** /**
* 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<R>; toHaveJSProperty(name: string, value: any, options?: { timeout?: number }): Promise<Locator>;
/** /**
* Asserts element's text content. * Asserts element's text content.
*/ */
toHaveText(expected: string | RegExp | (string|RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<R>; toHaveText(expected: string | RegExp | (string | RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<Locator>;
/**
* Asserts page's title.
*/
toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise<R>;
/**
* Asserts page's URL.
*/
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<R>;
/** /**
* Asserts input element's value. * Asserts input element's value.
*/ */
toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<R>; toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<Locator>;
/**
* Asserts given DOM node visible on the screen.
*/
toBeVisible(options?: { timeout?: number }): Promise<Locator>;
} }
interface PageMatchers {
/**
* Asserts page's title.
*/
toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
/**
* Asserts page's URL.
*/
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
} }
interface APIResponseMatchers {
/**
* Asserts given APIResponse's status is between 200 and 299.
*/
toBeOK(): Promise<APIResponse>;
} }
export { }; export { };

View file

@ -144,29 +144,29 @@ test('should work with custom PlaywrightTest namespace', async ({ runTSC }) => {
}); });
test('should propose only the relevant matchers when custom expect matcher classes were passed', async ({ runTSC }) => { test('should propose only the relevant matchers when custom expect matcher classes were passed', async ({ runTSC }) => {
test.fixme();
const result = await runTSC({ const result = await runTSC({
'a.spec.ts': ` 'a.spec.ts': `
const { test } = pwt; const { test } = pwt;
test('custom matchers', async ({ page }) => { test('custom matchers', async ({ page }) => {
await test.expect(page).toHaveURL('https://example.com'); await test.expect(page).toHaveURL('https://example.com');
// @ts-expect-error
await test.expect(page).toBe(true); await test.expect(page).toBe(true);
// @ts-expect-error // @ts-expect-error
await test.expect(page).toBeEnabled(); await test.expect(page).toBeEnabled();
await test.expect(page.locator('foo')).toBeEnabled(); await test.expect(page.locator('foo')).toBeEnabled();
// @ts-expect-error
await test.expect(page.locator('foo')).toBe(true); await test.expect(page.locator('foo')).toBe(true);
// @ts-expect-error // @ts-expect-error
await test.expect(page.locator('foo')).toHaveURL('https://example.com'); await test.expect(page.locator('foo')).toHaveURL('https://example.com');
const res = await page.request.get('http://i-do-definitely-not-exist.com'); const res = await page.request.get('http://i-do-definitely-not-exist.com');
await test.expect(res).toBeOK(); await test.expect(res).toBeOK();
// @ts-expect-error
await test.expect(res).toBe(true); await test.expect(res).toBe(true);
// @ts-expect-error // @ts-expect-error
await test.expect(res).toHaveURL('https://example.com'); await test.expect(res).toHaveURL('https://example.com');
await test.expect(res as any).toHaveURL('https://example.com');
// @ts-expect-error
await test.expect(123).toHaveURL('https://example.com');
}); });
` `
}); });