From 1e1df6395fe69266f77e95e089a095f9fc14f198 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 11 Apr 2022 10:42:19 -0700 Subject: [PATCH] chore: generate expect types (#13439) --- docs/src/api/class-screenshotassertions.md | 57 +- packages/playwright-test/types/test.d.ts | 697 +++++++++++++++++- .../playwright-test/types/testExpect.d.ts | 246 ------- tests/playwright-test/expect-poll.spec.ts | 7 +- tests/playwright-test/expect.spec.ts | 3 + utils/generate_types/index.js | 45 +- utils/generate_types/overrides-test.d.ts | 122 ++- 7 files changed, 886 insertions(+), 291 deletions(-) delete mode 100644 packages/playwright-test/types/testExpect.d.ts diff --git a/docs/src/api/class-screenshotassertions.md b/docs/src/api/class-screenshotassertions.md index cd7e8bf376..a8a3c63619 100644 --- a/docs/src/api/class-screenshotassertions.md +++ b/docs/src/api/class-screenshotassertions.md @@ -8,9 +8,7 @@ expected values stored in files. expect(screenshot).toMatchSnapshot('landing-page.png'); ``` - - -## method: ScreenshotAssertions.toMatchSnapshot +## method: ScreenshotAssertions.toMatchSnapshot#1 Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots directory. @@ -18,11 +16,8 @@ Ensures that passed value, either a [string] or a [Buffer], matches the expected // Basic usage. expect(await page.screenshot()).toMatchSnapshot('landing-page.png'); -// Basic usage and the file name is derived from the test name. -expect(await page.screenshot()).toMatchSnapshot(); - // Pass options to customize the snapshot comparison and have a generated name. -expect(await page.screenshot()).toMatchSnapshot({ +expect(await page.screenshot()).toMatchSnapshot('landing-page.png', { maxDiffPixels: 27, // allow no more than 27 different pixels. }); @@ -36,13 +31,49 @@ expect(await page.screenshot()).toMatchSnapshot(['landing', 'step3.png']); Learn more about [visual comparisons](./test-snapshots.md). -### param: ScreenshotAssertions.toMatchSnapshot.nameOrOptions -- `nameOrOptions` <[string]|[Array]<[string]>|[Object]> +### param: ScreenshotAssertions.toMatchSnapshot#1.name +- `name` <[string]|[Array]<[string]>> -Optional snapshot name. If not passed, the test name and ordinals are used when called multiple times. Also passing the options here is supported. +Snapshot name. -### option: ScreenshotAssertions.toMatchSnapshot.maxDiffPixels = %%-assertions-max-diff-pixels-%% +### option: ScreenshotAssertions.toMatchSnapshot#1.maxDiffPixels = %%-assertions-max-diff-pixels-%% -### option: ScreenshotAssertions.toMatchSnapshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%% +### option: ScreenshotAssertions.toMatchSnapshot#1.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%% + +### option: ScreenshotAssertions.toMatchSnapshot#1.threshold = %%-assertions-threshold-%% + + + +## method: ScreenshotAssertions.toMatchSnapshot#2 + +Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots directory. + +```js +// Basic usage and the file name is derived from the test name. +expect(await page.screenshot()).toMatchSnapshot(); + +// Pass options to customize the snapshot comparison and have a generated name. +expect(await page.screenshot()).toMatchSnapshot({ + maxDiffPixels: 27, // allow no more than 27 different pixels. +}); + +// Configure image matching threshold and snapshot name. +expect(await page.screenshot()).toMatchSnapshot({ + name: 'landing-page.png', + threshold: 0.3, +}); +``` + +Learn more about [visual comparisons](./test-snapshots.md). + +### option: ScreenshotAssertions.toMatchSnapshot#2.maxDiffPixels = %%-assertions-max-diff-pixels-%% + +### option: ScreenshotAssertions.toMatchSnapshot#2.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%% + +### option: ScreenshotAssertions.toMatchSnapshot#2.name +- `name` <[string]|[Array]<[string]>> + +Snapshot name. If not passed, the test name and ordinals are used when called multiple times. + +### option: ScreenshotAssertions.toMatchSnapshot#2.threshold = %%-assertions-threshold-%% -### option: ScreenshotAssertions.toMatchSnapshot.threshold = %%-assertions-threshold-%% diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 859ae55ae7..ccb922c017 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -15,10 +15,7 @@ * limitations under the License. */ -import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials } from 'playwright-core'; -import type { Expect } from './testExpect'; - -export type { Expect } from './testExpect'; +import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse } from 'playwright-core'; export type ReporterDescription = ['dot'] | @@ -2802,7 +2799,7 @@ export interface PlaywrightTestOptions { * [fixtures.page](https://playwright.dev/docs/api/class-fixtures#fixtures-page). */ export interface PlaywrightWorkerArgs { - playwright: typeof import('..'); + playwright: typeof import('playwright-core'); /** * [Browser] instance is shared between all tests in the [same worker](https://playwright.dev/docs/test-parallel) - this makes testing efficient. * However, each test runs in an isolated [BrowserContext] and gets a fresh environment. @@ -2891,6 +2888,121 @@ export interface PlaywrightTestArgs { export type PlaywrightTestProject = Project; export type PlaywrightTestConfig = Config; +import type * as expectType from 'expect'; + +type AsymmetricMatcher = Record; + +type IfAny = 0 extends (1 & T) ? Y : N; +type ExtraMatchers = T extends Type ? Matchers : IfAny; + +type BaseMatchers = Pick, SupportedExpectProperties> & PlaywrightTest.Matchers; + +type MakeMatchers = BaseMatchers & { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + 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, 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, Awaited>; + } & ScreenshotAssertions & + 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 }) => BaseMatchers, T> & { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: BaseMatchers, T>; + }; + + extend(arg0: any): void; + getState(): expectType.MatcherState; + setState(state: Partial): void; + any(expectedObject: any): AsymmetricMatcher; + anything(): AsymmetricMatcher; + arrayContaining(sample: Array): AsymmetricMatcher; + objectContaining(sample: Record): AsymmetricMatcher; + stringContaining(expected: string): AsymmetricMatcher; + stringMatching(expected: string | RegExp): AsymmetricMatcher; + /** + * Removed following methods because they rely on a test-runner integration from Jest which we don't support: + * - assertions() + * - extractExpectedAssertionsErrors() + * – hasAssertions() + */ +}; + +type Awaited = T extends PromiseLike ? U : T; + +/** + * Removed methods require the jest.fn() integration from Jest to spy on function calls which we don't support: + * - lastCalledWith() + * - lastReturnedWith() + * - nthCalledWith() + * - nthReturnedWith() + * - toBeCalled() + * - toBeCalledTimes() + * - toBeCalledWith() + * - toHaveBeenCalled() + * - toHaveBeenCalledTimes() + * - toHaveBeenCalledWith() + * - toHaveBeenLastCalledWith() + * - toHaveBeenNthCalledWith() + * - toHaveLastReturnedWith() + * - toHaveNthReturnedWith() + * - toHaveReturned() + * - toHaveReturnedTimes() + * - toHaveReturnedWith() + * - toReturn() + * - toReturnTimes() + * - toReturnWith() + * - toThrowErrorMatchingSnapshot() + * - toThrowErrorMatchingInlineSnapshot() + */ +type SupportedExpectProperties = + 'toBe' | + 'toBeCloseTo' | + 'toBeDefined' | + 'toBeFalsy' | + 'toBeGreaterThan' | + 'toBeGreaterThanOrEqual' | + 'toBeInstanceOf' | + 'toBeLessThan' | + 'toBeLessThanOrEqual' | + 'toBeNaN' | + 'toBeNull' | + 'toBeTruthy' | + 'toBeUndefined' | + 'toContain' | + 'toContainEqual' | + 'toEqual' | + 'toHaveLength' | + 'toHaveProperty' | + 'toMatch' | + 'toMatchObject' | + 'toStrictEqual' | + 'toThrow' | + 'toThrowError' + +declare global { + export namespace PlaywrightTest { + export interface Matchers { + } + } +} + /** * These tests are executed in Playwright environment that launches the browser * and provides a fresh page to each test. @@ -2905,6 +3017,581 @@ export const expect: Expect; export {}; +/** + * The [APIResponseAssertions] class provides assertion methods that can be used to make assertions about the [APIResponse] + * in the tests. A new instance of [APIResponseAssertions] is created by calling + * [expect(response)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-api-response): + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('navigates to login', async ({ page }) => { + * // ... + * const response = await page.request.get('https://playwright.dev'); + * await expect(response).toBeOK(); + * }); + * ``` + * + */ +interface APIResponseAssertions { + /** + * Makes the assertion check for the opposite condition. For example, this code tests that the response status is not + * successful: + * + * ```js + * await expect(response).not.toBeOK(); + * ``` + * + */ + not: APIResponseAssertions; + + /** + * Ensures the response status code is within [200..299] range. + * + * ```js + * await expect(response).toBeOK(); + * ``` + * + */ + toBeOK(): Promise; +} + +/** + * The [LocatorAssertions] class provides assertion methods that can be used to make assertions about the [Locator] state + * in the tests. A new instance of [LocatorAssertions] is created by calling + * [expect(locator)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-locator): + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('status becomes submitted', async ({ page }) => { + * // ... + * await page.click('#submit-button'); + * await expect(page.locator('.status')).toHaveText('Submitted'); + * }); + * ``` + * + */ +interface LocatorAssertions { + /** + * Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text + * `"error"`: + * + * ```js + * await expect(locator).not.toContainText('error'); + * ``` + * + */ + not: LocatorAssertions; + + /** + * Ensures the [Locator] points to a checked input. + * + * ```js + * const locator = page.locator('.subscribe'); + * await expect(locator).toBeChecked(); + * ``` + * + * @param options + */ + toBeChecked(options?: { + checked?: boolean; + + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to a disabled element. + * + * ```js + * const locator = page.locator('button.submit'); + * await expect(locator).toBeDisabled(); + * ``` + * + * @param options + */ + toBeDisabled(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an editable element. + * + * ```js + * const locator = page.locator('input'); + * await expect(locator).toBeEditable(); + * ``` + * + * @param options + */ + toBeEditable(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an empty editable element or to a DOM node that has no text. + * + * ```js + * const locator = page.locator('div.warning'); + * await expect(locator).toBeEmpty(); + * ``` + * + * @param options + */ + toBeEmpty(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an enabled element. + * + * ```js + * const locator = page.locator('button.submit'); + * await expect(locator).toBeEnabled(); + * ``` + * + * @param options + */ + toBeEnabled(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to a focused DOM node. + * + * ```js + * const locator = page.locator('input'); + * await expect(locator).toBeFocused(); + * ``` + * + * @param options + */ + toBeFocused(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to a hidden DOM node, which is the opposite of [visible](https://playwright.dev/docs/api/actionability#visible). + * + * ```js + * const locator = page.locator('.my-element'); + * await expect(locator).toBeHidden(); + * ``` + * + * @param options + */ + toBeHidden(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to a [visible](https://playwright.dev/docs/api/actionability#visible) DOM node. + * + * ```js + * const locator = page.locator('.my-element'); + * await expect(locator).toBeVisible(); + * ``` + * + * @param options + */ + toBeVisible(options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an element that contains the given text. You can use regular expressions for the value + * as well. + * + * ```js + * const locator = page.locator('.title'); + * await expect(locator).toContainText('substring'); + * await expect(locator).toContainText(/\d messages/); + * ``` + * + * Note that if array is passed as an expected value, entire lists of elements can be asserted: + * + * ```js + * const locator = page.locator('list > .list-item'); + * await expect(locator).toContainText(['Text 1', 'Text 4', 'Text 5']); + * ``` + * + * @param expected Expected substring or RegExp or a list of those. + * @param options + */ + toContainText(expected: string|RegExp|Array, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + + /** + * Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. + */ + useInnerText?: boolean; + }): Promise; + + /** + * Ensures the [Locator] points to an element with given attribute. + * + * ```js + * const locator = page.locator('input'); + * await expect(locator).toHaveAttribute('type', 'text'); + * ``` + * + * @param name Attribute name. + * @param value Expected attribute value. + * @param options + */ + toHaveAttribute(name: string, value: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an element with given CSS class. + * + * ```js + * const locator = page.locator('#component'); + * await expect(locator).toHaveClass(/selected/); + * ``` + * + * Note that if array is passed as an expected value, entire lists of elements can be asserted: + * + * ```js + * const locator = page.locator('list > .component'); + * await expect(locator).toHaveClass(['component', 'component selected', 'component']); + * ``` + * + * @param expected Expected class or RegExp or a list of those. + * @param options + */ + toHaveClass(expected: string|RegExp|Array, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] resolves to an exact number of DOM nodes. + * + * ```js + * const list = page.locator('list > .component'); + * await expect(list).toHaveCount(3); + * ``` + * + * @param count Expected count. + * @param options + */ + toHaveCount(count: number, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] resolves to an element with the given computed CSS style. + * + * ```js + * const locator = page.locator('button'); + * await expect(locator).toHaveCSS('display', 'flex'); + * ``` + * + * @param name CSS property name. + * @param value CSS property value. + * @param options + */ + toHaveCSS(name: string, value: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an element with the given DOM Node ID. + * + * ```js + * const locator = page.locator('input'); + * await expect(locator).toHaveId('lastname'); + * ``` + * + * @param id Element id. + * @param options + */ + toHaveId(id: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an element with given JavaScript property. Note that this property can be of a primitive + * type as well as a plain serializable JavaScript object. + * + * ```js + * const locator = page.locator('.component'); + * await expect(locator).toHaveJSProperty('loaded', true); + * ``` + * + * @param name Property name. + * @param value Property value. + * @param options + */ + toHaveJSProperty(name: string, value: any, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the [Locator] points to an element with the given text. You can use regular expressions for the value as well. + * + * ```js + * const locator = page.locator('.title'); + * await expect(locator).toHaveText(/Welcome, Test User/); + * await expect(locator).toHaveText(/Welcome, .*\/); + * ``` + * + * Note that if array is passed as an expected value, entire lists of elements can be asserted: + * + * ```js + * const locator = page.locator('list > .component'); + * await expect(locator).toHaveText(['Text 1', 'Text 2', 'Text 3']); + * ``` + * + * @param expected Expected substring or RegExp or a list of those. + * @param options + */ + toHaveText(expected: string|RegExp|Array, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + + /** + * Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. + */ + useInnerText?: boolean; + }): Promise; + + /** + * Ensures the [Locator] points to an element with the given input value. You can use regular expressions for the value as + * well. + * + * ```js + * const locator = page.locator('input[type=number]'); + * await expect(locator).toHaveValue(/[0-9]/); + * ``` + * + * @param value Expected value. + * @param options + */ + toHaveValue(value: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; +} + +/** + * The [PageAssertions] class provides assertion methods that can be used to make assertions about the [Page] state in the + * tests. A new instance of [PageAssertions] is created by calling + * [expect(page)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-page): + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('navigates to login', async ({ page }) => { + * // ... + * await page.click('#login'); + * await expect(page).toHaveURL(/.*\/login/); + * }); + * ``` + * + */ +interface PageAssertions { + /** + * Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain + * `"error"`: + * + * ```js + * await expect(page).not.toHaveURL('error'); + * ``` + * + */ + not: PageAssertions; + + /** + * Ensures the page has the given title. + * + * ```js + * await expect(page).toHaveTitle(/.*checkout/); + * ``` + * + * @param titleOrRegExp Expected title or RegExp. + * @param options + */ + toHaveTitle(titleOrRegExp: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + + /** + * Ensures the page is navigated to the given URL. + * + * ```js + * await expect(page).toHaveURL(/.*checkout/); + * ``` + * + * @param urlOrRegExp Expected substring or RegExp. + * @param options + */ + toHaveURL(urlOrRegExp: string|RegExp, options?: { + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; +} + +/** + * Playwright provides methods for comparing page and element screenshots with expected values stored in files. + * + * ```js + * expect(screenshot).toMatchSnapshot('landing-page.png'); + * ``` + * + */ +interface ScreenshotAssertions { + /** + * Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots + * directory. + * + * ```js + * // Basic usage. + * expect(await page.screenshot()).toMatchSnapshot('landing-page.png'); + * + * // Pass options to customize the snapshot comparison and have a generated name. + * expect(await page.screenshot()).toMatchSnapshot('landing-page.png', { + * maxDiffPixels: 27, // allow no more than 27 different pixels. + * }); + * + * // Configure image matching threshold. + * expect(await page.screenshot()).toMatchSnapshot('landing-page.png', { threshold: 0.3 }); + * + * // Bring some structure to your snapshot files by passing file path segments. + * expect(await page.screenshot()).toMatchSnapshot(['landing', 'step2.png']); + * expect(await page.screenshot()).toMatchSnapshot(['landing', 'step3.png']); + * ``` + * + * Learn more about [visual comparisons](https://playwright.dev/docs/api/test-snapshots). + * @param name Snapshot name. + * @param options + */ + toMatchSnapshot(name: string|Array, options?: { + /** + * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is + * configurable with `TestConfig.expect`. Unset by default. + */ + maxDiffPixelRatio?: number; + + /** + * An acceptable amount of pixels that could be different, default is configurable with `TestConfig.expect`. Default is + * configurable with `TestConfig.expect`. Unset by default. + */ + maxDiffPixels?: number; + + /** + * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same + * pixel in compared images, between zero (strict) and one (lax), default is configurable with `TestConfig.expect`. + * Defaults to `0.2`. + */ + threshold?: number; + }): void; + + /** + * Ensures that passed value, either a [string] or a [Buffer], matches the expected snapshot stored in the test snapshots + * directory. + * + * ```js + * // Basic usage and the file name is derived from the test name. + * expect(await page.screenshot()).toMatchSnapshot(); + * + * // Pass options to customize the snapshot comparison and have a generated name. + * expect(await page.screenshot()).toMatchSnapshot({ + * maxDiffPixels: 27, // allow no more than 27 different pixels. + * }); + * + * // Configure image matching threshold and snapshot name. + * expect(await page.screenshot()).toMatchSnapshot({ + * name: 'landing-page.png', + * threshold: 0.3, + * }); + * ``` + * + * Learn more about [visual comparisons](https://playwright.dev/docs/api/test-snapshots). + * @param options + */ + toMatchSnapshot(options?: { + /** + * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is + * configurable with `TestConfig.expect`. Unset by default. + */ + maxDiffPixelRatio?: number; + + /** + * An acceptable amount of pixels that could be different, default is configurable with `TestConfig.expect`. Default is + * configurable with `TestConfig.expect`. Unset by default. + */ + maxDiffPixels?: number; + + /** + * Snapshot name. If not passed, the test name and ordinals are used when called multiple times. + */ + name?: string|Array; + + /** + * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same + * pixel in compared images, between zero (strict) and one (lax), default is configurable with `TestConfig.expect`. + * Defaults to `0.2`. + */ + threshold?: number; + }): void; +} + /** * Information about an error thrown during test execution. */ diff --git a/packages/playwright-test/types/testExpect.d.ts b/packages/playwright-test/types/testExpect.d.ts deleted file mode 100644 index 81b7e78626..0000000000 --- a/packages/playwright-test/types/testExpect.d.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type * as expect from 'expect'; -import type { Page, Locator, APIResponse, PageScreenshotOptions, LocatorScreenshotOptions } from 'playwright-core'; - -export declare type AsymmetricMatcher = Record; - -type IfAny = 0 extends (1 & T) ? Y : N; -type ExtraMatchers = T extends Type ? Matchers : IfAny; - -type MakeMatchers = PlaywrightTest.Matchers & - 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, T>, 'rejects' | 'resolves'>; - - extend(arg0: any): void; - getState(): expect.MatcherState; - setState(state: Partial): void; - any(expectedObject: any): AsymmetricMatcher; - anything(): AsymmetricMatcher; - arrayContaining(sample: Array): AsymmetricMatcher; - objectContaining(sample: Record): AsymmetricMatcher; - stringContaining(expected: string): AsymmetricMatcher; - stringMatching(expected: string | RegExp): AsymmetricMatcher; - /** - * Removed following methods because they rely on a test-runner integration from Jest which we don't support: - * - assertions() - * - extractExpectedAssertionsErrors() - * – hasAssertions() - */ -}; - -type ImageComparatorOptions = { - threshold?: number, - maxDiffPixels?: number, - maxDiffPixelRatio?: number, -}; - -type Awaited = T extends PromiseLike ? U : T; - -/** - * Removed methods require the jest.fn() integration from Jest to spy on function calls which we don't support: - * - lastCalledWith() - * - lastReturnedWith() - * - nthCalledWith() - * - nthReturnedWith() - * - toBeCalled() - * - toBeCalledTimes() - * - toBeCalledWith() - * - toHaveBeenCalled() - * - toHaveBeenCalledTimes() - * - toHaveBeenCalledWith() - * - toHaveBeenLastCalledWith() - * - toHaveBeenNthCalledWith() - * - toHaveLastReturnedWith() - * - toHaveNthReturnedWith() - * - toHaveReturned() - * - toHaveReturnedTimes() - * - toHaveReturnedWith() - * - toReturn() - * - toReturnTimes() - * - toReturnWith() - * - toThrowErrorMatchingSnapshot() - * - toThrowErrorMatchingInlineSnapshot() - */ -type SupportedExpectProperties = - 'toBe' | - 'toBeCloseTo' | - 'toBeDefined' | - 'toBeFalsy' | - 'toBeGreaterThan' | - 'toBeGreaterThanOrEqual' | - 'toBeInstanceOf' | - 'toBeLessThan' | - 'toBeLessThanOrEqual' | - 'toBeNaN' | - 'toBeNull' | - 'toBeTruthy' | - 'toBeUndefined' | - 'toContain' | - 'toContainEqual' | - 'toEqual' | - 'toHaveLength' | - 'toHaveProperty' | - 'toMatch' | - 'toMatchObject' | - 'toStrictEqual' | - 'toThrow' | - 'toThrowError' - -declare global { - export namespace PlaywrightTest { - export interface Matchers extends Pick, SupportedExpectProperties> { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - 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, 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, Awaited>; - /** - * Match snapshot - */ - toMatchSnapshot(options?: ImageComparatorOptions & { - name?: string | string[], - }): R; - /** - * Match snapshot - */ - toMatchSnapshot(name: string | string[], options?: ImageComparatorOptions): R; - } - } -} - -interface LocatorMatchers { - /** - * Asserts input is checked (or unchecked if { checked: false } is passed). - */ - toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise; - - /** - * Asserts input is disabled. - */ - toBeDisabled(options?: { timeout?: number }): Promise; - - /** - * Asserts input is editable. - */ - toBeEditable(options?: { timeout?: number }): Promise; - - /** - * Asserts given DOM node or input has no text content or no input value. - */ - toBeEmpty(options?: { timeout?: number }): Promise; - - /** - * Asserts input is enabled. - */ - toBeEnabled(options?: { timeout?: number }): Promise; - - /** - * Asserts given DOM is a focused (active) in document. - */ - toBeFocused(options?: { timeout?: number }): Promise; - - /** - * Asserts given DOM node is hidden or detached from DOM. - */ - 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; - - /** - * Asserts element's attributes `name` matches expected value. - */ - 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; - - /** - * Asserts number of DOM nodes matching given locator. - */ - 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; - - /** - * Asserts element's `id` attribute matches expected value. - */ - 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; - - /** - * Asserts element's text content. - */ - 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; - - /** - * Asserts given DOM node visible on the screen. - */ - toBeVisible(options?: { timeout?: number }): Promise; -} -interface PageMatchers { - /** - * Asserts page's title. - */ - toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise; - - /** - * Asserts page's URL. - */ - toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise; -} - -interface APIResponseMatchers { - /** - * Asserts given APIResponse's status is between 200 and 299. - */ - toBeOK(): Promise; -} - -export { }; diff --git a/tests/playwright-test/expect-poll.spec.ts b/tests/playwright-test/expect-poll.spec.ts index bcad7a14c0..170c37c1e6 100644 --- a/tests/playwright-test/expect-poll.spec.ts +++ b/tests/playwright-test/expect-poll.spec.ts @@ -45,7 +45,7 @@ test('should compile', async ({ runTSC }) => { const result = await runTSC({ 'a.spec.ts': ` const { test } = pwt; - test('should poll sync predicate', () => { + test('should poll sync predicate', async ({ page }) => { let i = 0; test.expect.poll(() => ++i).toBe(3); test.expect.poll(() => ++i, 'message').toBe(3); @@ -57,6 +57,11 @@ test('should compile', async ({ runTSC }) => { return ++i; }).toBe(3); test.expect.poll(() => Promise.resolve(++i)).toBe(3); + + // @ts-expect-error + await test.expect.poll(() => page.locator('foo')).toBeEnabled(); + // @ts-expect-error + await test.expect.poll(() => page.locator('foo')).not.toBeEnabled(); }); ` }); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index f5b44e72f2..960f2dc398 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -212,9 +212,12 @@ test('should propose only the relevant matchers when custom expect matcher class const { test } = pwt; test('custom matchers', async ({ page }) => { await test.expect(page).toHaveURL('https://example.com'); + await test.expect(page).not.toHaveURL('https://example.com'); await test.expect(page).toBe(true); // @ts-expect-error await test.expect(page).toBeEnabled(); + // @ts-expect-error + await test.expect(page).not.toBeEnabled(); await test.expect(page.locator('foo')).toBeEnabled(); await test.expect(page.locator('foo')).toBe(true); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 9f47ce4dc7..96a6a62c42 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -34,9 +34,10 @@ class TypesGenerator { /** * @param {{ * documentation: Documentation, - * classNames: Set, - * overridesToDocsClassMapping: Map, - * ignoreMissing: Set, + * classNamesToGenerate: Set, + * overridesToDocsClassMapping?: Map, + * ignoreMissing?: Set, + * doNotExportClassNames?: Set, * }} options */ constructor(options) { @@ -45,9 +46,10 @@ class TypesGenerator { /** @type {Set} */ this.handledMethods = new Set(); this.documentation = options.documentation; - this.classNames = options.classNames; - this.overridesToDocsClassMapping = options.overridesToDocsClassMapping; - this.ignoreMissing = options.ignoreMissing; + this.classNamesToGenerate = options.classNamesToGenerate; + this.overridesToDocsClassMapping = options.overridesToDocsClassMapping || new Map(); + this.ignoreMissing = options.ignoreMissing || new Set(); + this.doNotExportClassNames = options.doNotExportClassNames || new Set(); } /** @@ -61,7 +63,7 @@ class TypesGenerator { const createMarkdownLink = (member, text) => { const className = toKebabCase(member.clazz.name); const memberName = toKebabCase(member.name); - let hash = null + let hash = null; if (member.kind === 'property' || member.kind === 'method') hash = `${className}-${memberName}`.toLowerCase(); else if (member.kind === 'event') @@ -76,12 +78,13 @@ class TypesGenerator { return `\`${option}\``; if (clazz) return `[${clazz.name}]`; + const className = member.clazz.varName === 'playwrightAssertions' ? '' : member.clazz.varName + '.'; if (member.kind === 'method') - return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}(${this.renderJSSignature(member.argsArray)})`); + return createMarkdownLink(member, `${className}${member.alias}(${this.renderJSSignature(member.argsArray)})`); if (member.kind === 'event') - return createMarkdownLink(member, `${member.clazz.varName}.on('${member.alias.toLowerCase()}')`); + return createMarkdownLink(member, `${className}on('${member.alias.toLowerCase()}')`); if (member.kind === 'property') - return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}`); + return createMarkdownLink(member, `${className}${member.alias}`); throw new Error('Unknown member kind ' + member.kind); }); this.documentation.generateSourceCodeComments(); @@ -113,13 +116,13 @@ class TypesGenerator { return this.memberJSDOC(method, ' ').trimLeft(); }, (className) => { const docClass = this.docClassForName(className); - if (!docClass || !this.classNames.has(docClass.name)) + if (!docClass || !this.classNamesToGenerate.has(docClass.name)) return ''; return this.classBody(docClass); }); const classes = this.documentation.classesArray - .filter(cls => this.classNames.has(cls.name)) + .filter(cls => this.classNamesToGenerate.has(cls.name)) .filter(cls => !handledClasses.has(cls.name)); { const playwright = this.documentation.classesArray.find(c => c.name === 'Playwright'); @@ -152,7 +155,7 @@ class TypesGenerator { * @param {string} name */ docClassForName(name) { - const mappedName = (this.overridesToDocsClassMapping ? this.overridesToDocsClassMapping.get(name) : undefined) || name; + const mappedName = this.overridesToDocsClassMapping.get(name) || name; const docClass = this.documentation.classes.get(mappedName); if (!docClass && !this.canIgnoreMissingName(name)) throw new Error(`Unknown override class ${name}`); @@ -189,7 +192,8 @@ class TypesGenerator { if (classDesc.comment) { parts.push(this.writeComment(classDesc.comment)) } - parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); + const shouldExport = !this.doNotExportClassNames.has(classDesc.name); + parts.push(`${shouldExport ? 'export ' : ''}interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); parts.push(this.classBody(classDesc)); parts.push('}\n'); return parts.join('\n'); @@ -497,14 +501,12 @@ class TypesGenerator { fs.mkdirSync(testTypesDir) writeFile(path.join(coreTypesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8')); - const assertionClasses = new Set(['PlaywrightAssertions', 'LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']); + const assertionClasses = new Set(['LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']); const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api')); apiDocumentation.index(); const apiTypesGenerator = new TypesGenerator({ documentation: apiDocumentation, - classNames: new Set(apiDocumentation.classesArray.map(cls => cls.name).filter(name => !assertionClasses.has(name))), - overridesToDocsClassMapping: new Map(), - ignoreMissing: new Set(), + classNamesToGenerate: new Set(apiDocumentation.classesArray.map(cls => cls.name).filter(name => !assertionClasses.has(name) && name !== 'PlaywrightAssertions')), }); let apiTypes = await apiTypesGenerator.generateTypes(path.join(__dirname, 'overrides.d.ts')); const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n'); @@ -530,7 +532,7 @@ class TypesGenerator { const testDocumentation = apiDocumentation.mergeWith(testOnlyDocumentation); const testTypesGenerator = new TypesGenerator({ documentation: testDocumentation, - classNames: new Set(['TestError', 'TestInfo', 'WorkerInfo']), + classNamesToGenerate: new Set(['TestError', 'TestInfo', 'WorkerInfo', ...assertionClasses]), overridesToDocsClassMapping: new Map([ ['TestType', 'Test'], ['Config', 'TestConfig'], @@ -548,7 +550,9 @@ class TypesGenerator { 'TestFunction', 'PlaywrightWorkerOptions.defaultBrowserType', 'PlaywrightWorkerArgs.playwright', + 'Matchers', ]), + doNotExportClassNames: new Set(assertionClasses), }); let testTypes = await testTypesGenerator.generateTypes(path.join(__dirname, 'overrides-test.d.ts')); testTypes = testTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace @@ -558,8 +562,7 @@ class TypesGenerator { const testReporterDocumentation = testDocumentation.mergeWith(testReporterOnlyDocumentation); const testReporterTypesGenerator = new TypesGenerator({ documentation: testReporterDocumentation, - classNames: new Set(testReporterOnlyDocumentation.classesArray.map(cls => cls.name)), - overridesToDocsClassMapping: new Map(), + classNamesToGenerate: new Set(testReporterOnlyDocumentation.classesArray.map(cls => cls.name)), ignoreMissing: new Set(['FullResult']), }); let testReporterTypes = await testReporterTypesGenerator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts')); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 790c48981a..249dc9f55d 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials } from 'playwright-core'; -import type { Expect } from './testExpect'; - -export type { Expect } from './testExpect'; +import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse } from 'playwright-core'; export type ReporterDescription = ['dot'] | @@ -332,7 +329,7 @@ export interface PlaywrightTestOptions { export interface PlaywrightWorkerArgs { - playwright: typeof import('..'); + playwright: typeof import('playwright-core'); browser: Browser; } @@ -345,6 +342,121 @@ export interface PlaywrightTestArgs { export type PlaywrightTestProject = Project; export type PlaywrightTestConfig = Config; +import type * as expectType from 'expect'; + +type AsymmetricMatcher = Record; + +type IfAny = 0 extends (1 & T) ? Y : N; +type ExtraMatchers = T extends Type ? Matchers : IfAny; + +type BaseMatchers = Pick, SupportedExpectProperties> & PlaywrightTest.Matchers; + +type MakeMatchers = BaseMatchers & { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + 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, 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, Awaited>; + } & ScreenshotAssertions & + 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 }) => BaseMatchers, T> & { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: BaseMatchers, T>; + }; + + extend(arg0: any): void; + getState(): expectType.MatcherState; + setState(state: Partial): void; + any(expectedObject: any): AsymmetricMatcher; + anything(): AsymmetricMatcher; + arrayContaining(sample: Array): AsymmetricMatcher; + objectContaining(sample: Record): AsymmetricMatcher; + stringContaining(expected: string): AsymmetricMatcher; + stringMatching(expected: string | RegExp): AsymmetricMatcher; + /** + * Removed following methods because they rely on a test-runner integration from Jest which we don't support: + * - assertions() + * - extractExpectedAssertionsErrors() + * – hasAssertions() + */ +}; + +type Awaited = T extends PromiseLike ? U : T; + +/** + * Removed methods require the jest.fn() integration from Jest to spy on function calls which we don't support: + * - lastCalledWith() + * - lastReturnedWith() + * - nthCalledWith() + * - nthReturnedWith() + * - toBeCalled() + * - toBeCalledTimes() + * - toBeCalledWith() + * - toHaveBeenCalled() + * - toHaveBeenCalledTimes() + * - toHaveBeenCalledWith() + * - toHaveBeenLastCalledWith() + * - toHaveBeenNthCalledWith() + * - toHaveLastReturnedWith() + * - toHaveNthReturnedWith() + * - toHaveReturned() + * - toHaveReturnedTimes() + * - toHaveReturnedWith() + * - toReturn() + * - toReturnTimes() + * - toReturnWith() + * - toThrowErrorMatchingSnapshot() + * - toThrowErrorMatchingInlineSnapshot() + */ +type SupportedExpectProperties = + 'toBe' | + 'toBeCloseTo' | + 'toBeDefined' | + 'toBeFalsy' | + 'toBeGreaterThan' | + 'toBeGreaterThanOrEqual' | + 'toBeInstanceOf' | + 'toBeLessThan' | + 'toBeLessThanOrEqual' | + 'toBeNaN' | + 'toBeNull' | + 'toBeTruthy' | + 'toBeUndefined' | + 'toContain' | + 'toContainEqual' | + 'toEqual' | + 'toHaveLength' | + 'toHaveProperty' | + 'toMatch' | + 'toMatchObject' | + 'toStrictEqual' | + 'toThrow' | + 'toThrowError' + +declare global { + export namespace PlaywrightTest { + export interface Matchers { + } + } +} + /** * These tests are executed in Playwright environment that launches the browser * and provides a fresh page to each test.