From bc4106a7a60a208e59395bdab3c6c0d9032f250c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 11 Mar 2026 02:18:22 +0000 Subject: [PATCH] Revert "feat(expect): toHaveCSS with object notation (#38982)" This reverts commit 750fc9406c418e7f482dcbb8ca9b2c9798d19169. --- docs/src/actionability.md | 2 +- docs/src/api/class-locatorassertions.md | 39 +- docs/src/release-notes-js.md | 2 +- .../src/test-assertions-csharp-java-python.md | 2 +- docs/src/test-assertions-js.md | 2 +- packages/injected/src/injectedScript.ts | 28 +- .../playwright-core/src/protocol/validator.ts | 2 +- packages/playwright-core/src/server/frames.ts | 15 +- packages/playwright/src/matchers/matchers.ts | 26 +- packages/playwright/types/test.d.ts | 441 +++++++++--------- packages/protocol/src/channels.d.ts | 4 +- packages/protocol/src/protocol.yml | 2 +- tests/page/expect-misc.spec.ts | 51 -- utils/generate_types/overrides-test.d.ts | 7 - 14 files changed, 243 insertions(+), 380 deletions(-) diff --git a/docs/src/actionability.md b/docs/src/actionability.md index cff339faffb01..3510171e5c328 100644 --- a/docs/src/actionability.md +++ b/docs/src/actionability.md @@ -66,7 +66,7 @@ Playwright includes auto-retrying assertions that remove flakiness by waiting un | [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute | | [`method: LocatorAssertions.toHaveClass`] | Element has a class property | | [`method: LocatorAssertions.toHaveCount`] | List has exact number of children | -| [`method: LocatorAssertions.toHaveCSS#1`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property | | [`method: LocatorAssertions.toHaveId`] | Element has an ID | | [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property | | [`method: LocatorAssertions.toHaveText`] | Element matches text | diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 0b9d382a259e1..1dec8cf901932 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -355,7 +355,7 @@ Expected count. * since: v1.20 * langs: python -The opposite of [`method: LocatorAssertions.toHaveCSS#1`]. +The opposite of [`method: LocatorAssertions.toHaveCSS`]. ### param: LocatorAssertions.NotToHaveCSS.name * since: v1.18 @@ -1698,7 +1698,7 @@ Expected count. ### option: LocatorAssertions.toHaveCount.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 -## async method: LocatorAssertions.toHaveCSS#1 +## async method: LocatorAssertions.toHaveCSS * since: v1.20 * langs: - alias-java: hasCSS @@ -1735,51 +1735,24 @@ var locator = Page.GetByRole(AriaRole.Button); await Expect(locator).ToHaveCSSAsync("display", "flex"); ``` -### param: LocatorAssertions.toHaveCSS#1.name +### param: LocatorAssertions.toHaveCSS.name * since: v1.18 - `name` <[string]> CSS property name. -### param: LocatorAssertions.toHaveCSS#1.value +### param: LocatorAssertions.toHaveCSS.value * since: v1.18 - `value` <[string]|[RegExp]> CSS property value. -### option: LocatorAssertions.toHaveCSS#1.timeout = %%-js-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS.timeout = %%-js-assertions-timeout-%% * since: v1.18 -### option: LocatorAssertions.toHaveCSS#1.timeout = %%-csharp-java-python-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 - -## async method: LocatorAssertions.toHaveCSS#2 -* since: v1.58 -* langs: js - -Ensures the [Locator] resolves to an element with the given computed CSS properties. Only the listed properties are checked. - -**Usage** - -```js -const locator = page.getByRole('button'); -await expect(locator).toHaveCSS({ - display: 'flex', - backgroundColor: 'rgb(255, 0, 0)' -}); -``` - -### param: LocatorAssertions.toHaveCSS#2.styles -* since: v1.58 -- `styles` <[Object]> - -CSS properties object. See [CSSStyleProperties](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleProperties) for available properties. - -### option: LocatorAssertions.toHaveCSS#2.timeout = %%-js-assertions-timeout-%% -* since: v1.58 - - ## async method: LocatorAssertions.toHaveId * since: v1.20 * langs: diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index ba0a6a14c6b47..c909043c107e8 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -3200,7 +3200,7 @@ List of all new assertions: - [`expect(locator).toHaveAttribute(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-attribute) - [`expect(locator).toHaveClass(expected)`](./api/class-locatorassertions#locator-assertions-to-have-class) - [`expect(locator).toHaveCount(count)`](./api/class-locatorassertions#locator-assertions-to-have-count) -- [`expect(locator).toHaveCSS(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-css-1) +- [`expect(locator).toHaveCSS(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-css) - [`expect(locator).toHaveId(id)`](./api/class-locatorassertions#locator-assertions-to-have-id) - [`expect(locator).toHaveJSProperty(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-js-property) - [`expect(locator).toHaveText(expected, options)`](./api/class-locatorassertions#locator-assertions-to-have-text) diff --git a/docs/src/test-assertions-csharp-java-python.md b/docs/src/test-assertions-csharp-java-python.md index 520a9419b87e3..114de1624a8cc 100644 --- a/docs/src/test-assertions-csharp-java-python.md +++ b/docs/src/test-assertions-csharp-java-python.md @@ -24,7 +24,7 @@ title: "Assertions" | [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute | | [`method: LocatorAssertions.toHaveClass`] | Element has a class property | | [`method: LocatorAssertions.toHaveCount`] | List has exact number of children | -| [`method: LocatorAssertions.toHaveCSS#1`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property | | [`method: LocatorAssertions.toHaveId`] | Element has an ID | | [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property | | [`method: LocatorAssertions.toHaveRole`] | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) | diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md index 7baca2e707736..c809dce782764 100644 --- a/docs/src/test-assertions-js.md +++ b/docs/src/test-assertions-js.md @@ -46,7 +46,7 @@ Note that retrying assertions are async, so you must `await` them. | [await expect(locator).toHaveAttribute()](./api/class-locatorassertions.md#locator-assertions-to-have-attribute) | Element has a DOM attribute | | [await expect(locator).toHaveClass()](./api/class-locatorassertions.md#locator-assertions-to-have-class) | Element has specified CSS class property | | [await expect(locator).toHaveCount()](./api/class-locatorassertions.md#locator-assertions-to-have-count) | List has exact number of children | -| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css-1) | Element has CSS property | +| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css) | Element has CSS property | | [await expect(locator).toHaveId()](./api/class-locatorassertions.md#locator-assertions-to-have-id) | Element has an ID | | [await expect(locator).toHaveJSProperty()](./api/class-locatorassertions.md#locator-assertions-to-have-js-property) | Element has a JavaScript property | | [await expect(locator).toHaveRole()](./api/class-locatorassertions.md#locator-assertions-to-have-role) | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) | diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 609fce22a914f..5ec25b19e5c19 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1423,7 +1423,7 @@ export class InjectedScript { // Element state / boolean values. let result: ElementStateQueryResult | undefined; if (expression === 'to.have.attribute') { - const hasAttribute = element.hasAttribute(options.expressionArg || ''); + const hasAttribute = element.hasAttribute(options.expressionArg); result = { matches: hasAttribute, received: hasAttribute ? 'attribute present' : 'attribute not present', @@ -1487,7 +1487,7 @@ export class InjectedScript { // JS property if (expression === 'to.have.property') { let target = element; - const properties = (options.expressionArg || '').split('.'); + const properties = options.expressionArg.split('.'); for (let i = 0; i < properties.length - 1; i++) { if (typeof target !== 'object' || !(properties[i] in target)) return { received: undefined, matches: false }; @@ -1498,26 +1498,6 @@ export class InjectedScript { return { received, matches }; } } - - { - // Computed style object - if (expression === 'to.have.css.object') { - const expected = (options.expectedValue ?? {}) as Record; - const received: Record = {}; - let matches = true; - const style = this.window.getComputedStyle(element); - for (const [prop, value] of Object.entries(expected)) { - let computed = style[prop as any]; - if (typeof computed !== 'string') - computed = ''; - if (computed !== value) - matches = false; - received[prop] = computed; - } - return { received, matches }; - } - } - { // Viewport intersection if (expression === 'to.be.in.viewport') { @@ -1554,7 +1534,7 @@ export class InjectedScript { // Single text value. let received: string | undefined; if (expression === 'to.have.attribute.value') { - const value = element.getAttribute(options.expressionArg || ''); + const value = element.getAttribute(options.expressionArg); if (value === null) return { received: null, matches: false }; received = value; @@ -1566,7 +1546,7 @@ export class InjectedScript { matches: new ExpectedTextMatcher(options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'), }; } else if (expression === 'to.have.css') { - received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg || ''); + received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg); } else if (expression === 'to.have.id') { received = element.id; } else if (expression === 'to.have.text') { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index b841a21e8923b..a9ee3039e8216 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1989,7 +1989,7 @@ scheme.FrameWaitForSelectorResult = tObject({ scheme.FrameExpectParams = tObject({ selector: tOptional(tString), expression: tString, - expressionArg: tOptional(tString), + expressionArg: tOptional(tAny), expectedText: tOptional(tArray(tType('ExpectedTextValue'))), expectedNumber: tOptional(tFloat), expectedValue: tOptional(tType('SerializedArgument')), diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b32396d002855..a09b41d050cff 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1480,11 +1480,8 @@ export class Frame extends SdkObject { lastIntermediateResult.received = received; } lastIntermediateResult.isSet = true; - if (!missingReceived) { - const rendered = renderUnexpectedValue(options.expression, received); - if (rendered !== undefined) - progress.log(` unexpected value "${rendered}"`); - } + if (!missingReceived && !Array.isArray(received)) + progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); } return { matches, received }; } @@ -1748,10 +1745,8 @@ function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.L return waitUntil; } -function renderUnexpectedValue(expression: string, received: any): string | undefined { +function renderUnexpectedValue(expression: string, received: any): string { if (expression === 'to.match.aria') - received = received?.raw; - if (Array.isArray(received) || (!!received && typeof received === 'object')) - return; - return String(received); + return received ? received.raw : received; + return received; } diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index d59c9681ac6b5..826b3a6c94f63 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -313,30 +313,18 @@ export function toHaveCount( }, expected, options); } -type ToHaveCSSOptions = { timeout?: number }; export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number }): Promise>; -export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, styles: Record, options?: { timeout?: number }): Promise>; export function toHaveCSS( this: ExpectMatcherStateInternal, locator: LocatorEx, - arg1: string | Record, - arg2?: string | RegExp | ToHaveCSSOptions, - arg3?: ToHaveCSSOptions, + name: string, + expected: string | RegExp, + options?: { timeout?: number }, ) { - if (typeof arg1 === 'string') { - if (arg2 === undefined || !(isString(arg2) || isRegExp(arg2))) - throw new Error(`toHaveCSS expected value must be a string or a regular expression`); - return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues([arg2]); - return await locator._expect('to.have.css', { expressionArg: arg1, expectedText, isNot, timeout }); - }, arg2, arg3); - } else { - if (typeof arg1 !== 'object' || !arg1) - throw new Error(`toHaveCSS argument must be a string or an object`); - return toEqual.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - return await locator._expect('to.have.css.object', { isNot, expectedValue: arg1, timeout }); - }, arg1, arg2 as (ToHaveCSSOptions | undefined)); - } + return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected]); + return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); + }, expected, options); } export function toHaveId( diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 742c4b9ebfa02..031c71d63deb9 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8418,60 +8418,224 @@ type FunctionAssertions = { toPass(options?: { timeout?: number, intervals?: number[] }): Promise; }; -type CSSStyleProperties = { [k in Exclude]?: CSSStyleDeclaration[k] extends Function ? never : CSSStyleDeclaration[k] }; +type BaseMatchers = GenericAssertions & PlaywrightTest.Matchers & SnapshotAssertions; +type AllowedGenericMatchers = PlaywrightTest.Matchers & Pick, 'toBe' | 'toBeDefined' | 'toBeFalsy' | 'toBeNull' | 'toBeTruthy' | 'toBeUndefined'>; + +type SpecificMatchers = + T extends Page ? PageAssertions & AllowedGenericMatchers : + T extends Locator ? LocatorAssertions & AllowedGenericMatchers : + T extends APIResponse ? APIResponseAssertions & AllowedGenericMatchers : + BaseMatchers & (T extends Function ? FunctionAssertions : {}); +type AllMatchers = PageAssertions & LocatorAssertions & APIResponseAssertions & FunctionAssertions & BaseMatchers; + +type IfAny = 0 extends (1 & T) ? Y : N; +type Awaited = T extends PromiseLike ? U : T; +type ToUserMatcher = F extends (first: any, ...args: infer Rest) => infer R ? (...args: Rest) => (R extends PromiseLike ? Promise : DefaultReturnType) : never; +type ToUserMatcherObject = { + [K in keyof T as T[K] extends (arg: ArgType, ...rest: any[]) => any ? K : never]: ToUserMatcher; +}; + +type MatcherHintColor = (arg: string) => string; + +export type MatcherHintOptions = { + comment?: string; + expectedColor?: MatcherHintColor; + isDirectExpectCall?: boolean; + isNot?: boolean; + promise?: string; + receivedColor?: MatcherHintColor; + secondArgument?: string; + secondArgumentColor?: MatcherHintColor; +}; + +export interface ExpectMatcherUtils { + matcherHint(matcherName: string, received: unknown, expected: unknown, options?: MatcherHintOptions): string; + printDiffOrStringify(expected: unknown, received: unknown, expectedLabel: string, receivedLabel: string, expand: boolean): string; + printExpected(value: unknown): string; + printReceived(object: unknown): string; + printWithType(name: string, value: T, print: (value: T) => string): string; + diff(a: unknown, b: unknown): string | null; + stringify(object: unknown, maxDepth?: number, maxWidth?: number): string; +} + +export type ExpectMatcherState = { + /** + * Whether this matcher was called with the negated .not modifier. + */ + isNot: boolean; + /** + * - 'rejects' if matcher was called with the promise .rejects modifier + * - 'resolves' if matcher was called with the promise .resolves modifier + * - '' if matcher was not called with a promise modifier + */ + promise: 'rejects' | 'resolves' | ''; + utils: ExpectMatcherUtils; + /** + * Timeout in milliseconds for the assertion to be fulfilled. + */ + timeout: number; +}; + +export type MatcherReturnType = { + message: () => string; + pass: boolean; + name?: string; + expected?: unknown; + actual?: any; + log?: string[]; + timeout?: number; +}; + +type MakeMatchers = { + /** + * 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, ExtendedMatchers>; + /** + * Unwraps the reason of a rejected promise so any other matcher can be chained. + * If the promise is fulfilled the assertion fails. + */ + rejects: MakeMatchers, any, ExtendedMatchers>; +} & IfAny, SpecificMatchers & ToUserMatcherObject>; + +type PollMatchers = { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: PollMatchers; +} & BaseMatchers & ToUserMatcherObject; + +export 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, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; + extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; + configure: (configuration: { + message?: string, + timeout?: number, + soft?: boolean, + }) => Expect; + getState(): unknown; + not: Omit; +} & AsymmetricMatchers; + +// --- BEGINGLOBAL --- +declare global { + export namespace PlaywrightTest { + export interface Matchers { + } + } +} +// --- ENDGLOBAL --- /** - * The [LocatorAssertions](https://playwright.dev/docs/api/class-locatorassertions) class provides assertion methods - * that can be used to make assertions about the [Locator](https://playwright.dev/docs/api/class-locator) state in the - * tests. + * These tests are executed in Playwright environment that launches the browser + * and provides a fresh page to each test. + */ +export const test: TestType; +export default test; + +export const _baseTest: TestType<{}, {}>; +export const expect: Expect<{}>; + +/** + * Defines Playwright config + */ +export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; + +type MergedT = List extends [TestType, ...(infer Rest)] ? T & MergedT : {}; +type MergedW = List extends [TestType, ...(infer Rest)] ? W & MergedW : {}; +type MergedTestType = TestType, MergedW>; + +/** + * Merges fixtures + */ +export function mergeTests(...tests: List): MergedTestType; + +type MergedExpectMatchers = List extends [Expect, ...(infer Rest)] ? M & MergedExpectMatchers : {}; +type MergedExpect = Expect>; + +/** + * Merges expects + */ +export function mergeExpects(...expects: List): MergedExpect; + +// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 +export { }; + + + +/** + * The [APIResponseAssertions](https://playwright.dev/docs/api/class-apiresponseassertions) class provides assertion + * methods that can be used to make assertions about the + * [APIResponse](https://playwright.dev/docs/api/class-apiresponse) in the tests. * * ```js * import { test, expect } from '@playwright/test'; * - * test('status becomes submitted', async ({ page }) => { + * test('navigates to login', async ({ page }) => { * // ... - * await page.getByRole('button').click(); - * await expect(page.locator('.status')).toHaveText('Submitted'); + * const response = await page.request.get('https://playwright.dev'); + * await expect(response).toBeOK(); * }); * ``` * */ -interface LocatorAssertions { +interface APIResponseAssertions { /** - * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) resolves to an element with the given computed - * CSS style. + * Ensures the response status code is within `200..299` range. * * **Usage** * * ```js - * const locator = page.getByRole('button'); - * await expect(locator).toHaveCSS('display', 'flex'); + * await expect(response).toBeOK(); * ``` * - * @param name CSS property name. - * @param value CSS property value. - * @param options */ - toHaveCSS(name: string, value: string|RegExp, options?: { timeout?: number }): Promise; + toBeOK(): Promise; + /** - * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) resolves to an element with the given computed - * CSS properties. Only the listed properties are checked. + * Makes the assertion check for the opposite condition. * * **Usage** * + * For example, this code tests that the response status is not successful: + * * ```js - * const locator = page.getByRole('button'); - * await expect(locator).toHaveCSS({ - * display: 'flex', - * backgroundColor: 'rgb(255, 0, 0)' - * }); + * await expect(response).not.toBeOK(); * ``` * - * @param styles CSS properties object. See - * [CSSStyleProperties](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleProperties) for available properties. - * @param options */ - toHaveCSS(values: CSSStyleProperties, options?: { timeout?: number }): Promise; + not: APIResponseAssertions; +} + +/** + * The [LocatorAssertions](https://playwright.dev/docs/api/class-locatorassertions) class provides assertion methods + * that can be used to make assertions about the [Locator](https://playwright.dev/docs/api/class-locator) state in the + * tests. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('status becomes submitted', async ({ page }) => { + * // ... + * await page.getByRole('button').click(); + * await expect(page.locator('.status')).toHaveText('Submitted'); + * }); + * ``` + * + */ +interface LocatorAssertions { /** * Ensures that [Locator](https://playwright.dev/docs/api/class-locator) points to an element that is * [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot. @@ -9024,6 +9188,28 @@ interface LocatorAssertions { timeout?: number; }): Promise; + /** + * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) resolves to an element with the given computed + * CSS style. + * + * **Usage** + * + * ```js + * const locator = page.getByRole('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 in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with the given DOM Node * ID. @@ -9468,207 +9654,6 @@ interface LocatorAssertions { not: LocatorAssertions; } -type BaseMatchers = GenericAssertions & PlaywrightTest.Matchers & SnapshotAssertions; -type AllowedGenericMatchers = PlaywrightTest.Matchers & Pick, 'toBe' | 'toBeDefined' | 'toBeFalsy' | 'toBeNull' | 'toBeTruthy' | 'toBeUndefined'>; - -type SpecificMatchers = - T extends Page ? PageAssertions & AllowedGenericMatchers : - T extends Locator ? LocatorAssertions & AllowedGenericMatchers : - T extends APIResponse ? APIResponseAssertions & AllowedGenericMatchers : - BaseMatchers & (T extends Function ? FunctionAssertions : {}); -type AllMatchers = PageAssertions & LocatorAssertions & APIResponseAssertions & FunctionAssertions & BaseMatchers; - -type IfAny = 0 extends (1 & T) ? Y : N; -type Awaited = T extends PromiseLike ? U : T; -type ToUserMatcher = F extends (first: any, ...args: infer Rest) => infer R ? (...args: Rest) => (R extends PromiseLike ? Promise : DefaultReturnType) : never; -type ToUserMatcherObject = { - [K in keyof T as T[K] extends (arg: ArgType, ...rest: any[]) => any ? K : never]: ToUserMatcher; -}; - -type MatcherHintColor = (arg: string) => string; - -export type MatcherHintOptions = { - comment?: string; - expectedColor?: MatcherHintColor; - isDirectExpectCall?: boolean; - isNot?: boolean; - promise?: string; - receivedColor?: MatcherHintColor; - secondArgument?: string; - secondArgumentColor?: MatcherHintColor; -}; - -export interface ExpectMatcherUtils { - matcherHint(matcherName: string, received: unknown, expected: unknown, options?: MatcherHintOptions): string; - printDiffOrStringify(expected: unknown, received: unknown, expectedLabel: string, receivedLabel: string, expand: boolean): string; - printExpected(value: unknown): string; - printReceived(object: unknown): string; - printWithType(name: string, value: T, print: (value: T) => string): string; - diff(a: unknown, b: unknown): string | null; - stringify(object: unknown, maxDepth?: number, maxWidth?: number): string; -} - -export type ExpectMatcherState = { - /** - * Whether this matcher was called with the negated .not modifier. - */ - isNot: boolean; - /** - * - 'rejects' if matcher was called with the promise .rejects modifier - * - 'resolves' if matcher was called with the promise .resolves modifier - * - '' if matcher was not called with a promise modifier - */ - promise: 'rejects' | 'resolves' | ''; - utils: ExpectMatcherUtils; - /** - * Timeout in milliseconds for the assertion to be fulfilled. - */ - timeout: number; -}; - -export type MatcherReturnType = { - message: () => string; - pass: boolean; - name?: string; - expected?: unknown; - actual?: any; - log?: string[]; - timeout?: number; -}; - -type MakeMatchers = { - /** - * 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, ExtendedMatchers>; - /** - * Unwraps the reason of a rejected promise so any other matcher can be chained. - * If the promise is fulfilled the assertion fails. - */ - rejects: MakeMatchers, any, ExtendedMatchers>; -} & IfAny, SpecificMatchers & ToUserMatcherObject>; - -type PollMatchers = { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: PollMatchers; -} & BaseMatchers & ToUserMatcherObject; - -export 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, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; - extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; - configure: (configuration: { - message?: string, - timeout?: number, - soft?: boolean, - }) => Expect; - getState(): unknown; - not: Omit; -} & AsymmetricMatchers; - -// --- BEGINGLOBAL --- -declare global { - export namespace PlaywrightTest { - export interface Matchers { - } - } -} -// --- ENDGLOBAL --- - -/** - * These tests are executed in Playwright environment that launches the browser - * and provides a fresh page to each test. - */ -export const test: TestType; -export default test; - -export const _baseTest: TestType<{}, {}>; -export const expect: Expect<{}>; - -/** - * Defines Playwright config - */ -export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; - -type MergedT = List extends [TestType, ...(infer Rest)] ? T & MergedT : {}; -type MergedW = List extends [TestType, ...(infer Rest)] ? W & MergedW : {}; -type MergedTestType = TestType, MergedW>; - -/** - * Merges fixtures - */ -export function mergeTests(...tests: List): MergedTestType; - -type MergedExpectMatchers = List extends [Expect, ...(infer Rest)] ? M & MergedExpectMatchers : {}; -type MergedExpect = Expect>; - -/** - * Merges expects - */ -export function mergeExpects(...expects: List): MergedExpect; - -// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 -export { }; - - - -/** - * The [APIResponseAssertions](https://playwright.dev/docs/api/class-apiresponseassertions) class provides assertion - * methods that can be used to make assertions about the - * [APIResponse](https://playwright.dev/docs/api/class-apiresponse) in the tests. - * - * ```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 { - /** - * Ensures the response status code is within `200..299` range. - * - * **Usage** - * - * ```js - * await expect(response).toBeOK(); - * ``` - * - */ - toBeOK(): Promise; - - /** - * Makes the assertion check for the opposite condition. - * - * **Usage** - * - * For example, this code tests that the response status is not successful: - * - * ```js - * await expect(response).not.toBeOK(); - * ``` - * - */ - not: APIResponseAssertions; -} - /** * The [PageAssertions](https://playwright.dev/docs/api/class-pageassertions) class provides assertion methods that * can be used to make assertions about the [Page](https://playwright.dev/docs/api/class-page) state in the tests. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 8222697e25f58..53fd92abf6c2c 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3422,7 +3422,7 @@ export type FrameWaitForSelectorResult = { export type FrameExpectParams = { selector?: string, expression: string, - expressionArg?: string, + expressionArg?: any, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, @@ -3432,7 +3432,7 @@ export type FrameExpectParams = { }; export type FrameExpectOptions = { selector?: string, - expressionArg?: string, + expressionArg?: any, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5f94b100ddbd9..57c3d0aa7db7c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2882,7 +2882,7 @@ Frame: parameters: selector: string? expression: string - expressionArg: string? + expressionArg: json? expectedText: type: array? items: ExpectedTextValue diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 34edb029b481e..ccedf8c06bf9a 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -527,57 +527,6 @@ test.describe('toHaveCSS', () => { const locator = page.locator('#node'); await expect(locator).toHaveCSS('--custom-color-property', '#FF00FF'); }); - - test('fail with bad arguments', async ({ page }) => { - await page.setContent(`
hi
`); - const locator = page.locator('#node'); - try { - await expect(locator).toHaveCSS('property', {} as any); - expect(true, 'should throw!').toBe(false); - } catch (error) { - expect(error.message).toContain(`toHaveCSS expected value must be a string or a regular expression`); - } - try { - await expect(locator).toHaveCSS(123 as any, '123'); - expect(true, 'should throw!').toBe(false); - } catch (error) { - expect(error.message).toContain(`toHaveCSS argument must be a string or an object`); - } - }); - - test('pass with object', async ({ page }) => { - await page.setContent(`
Text content
`); - const locator = page.locator('#node'); - await expect(locator).toHaveCSS({ - color: 'rgb(255, 0, 0)', - display: 'flex', - backgroundColor: 'rgb(255, 0, 0)', - }); - }); - - test('does not support custom properties inside object', async ({ page }) => { - await page.setContent(`
Text content
`); - const locator = page.locator('#node'); - await expect(locator).toHaveCSS({ '--my-color': '' } as any); - }); - - test('fail with object', async ({ page }) => { - await page.setContent(`
Text content
`); - const locator = page.locator('#node'); - const error = await expect(locator).toHaveCSS({ - 'color': 'blue', - 'display': 'flex', - 'backgroundColor': 'rgb(255, 0, 0)', - }, { timeout: 3000 }).catch(e => e); - expect(stripAnsi(error.message)).toContain(` - Object { - "backgroundColor": "rgb(255, 0, 0)", -- "color": "blue", -+ "color": "rgb(255, 0, 0)", - "display": "flex", - }`); - expect(stripAnsi(error.message)).not.toContain(`unexpected value "[object Object]"`); - }); }); test.describe('toHaveId', () => { diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 93bbfe5e2e4c2..92f132d608ea5 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -403,13 +403,6 @@ type FunctionAssertions = { toPass(options?: { timeout?: number, intervals?: number[] }): Promise; }; -type CSSStyleProperties = { [k in Exclude]?: CSSStyleDeclaration[k] extends Function ? never : CSSStyleDeclaration[k] }; - -interface LocatorAssertions { - toHaveCSS(name: string, value: string|RegExp, options?: { timeout?: number }): Promise; - toHaveCSS(values: CSSStyleProperties, options?: { timeout?: number }): Promise; -} - type BaseMatchers = GenericAssertions & PlaywrightTest.Matchers & SnapshotAssertions; type AllowedGenericMatchers = PlaywrightTest.Matchers & Pick, 'toBe' | 'toBeDefined' | 'toBeFalsy' | 'toBeNull' | 'toBeTruthy' | 'toBeUndefined'>;