diff --git a/docs/src/actionability.md b/docs/src/actionability.md index 3510171e5c328..cff339faffb01 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`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS#1`] | 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 4473a9a333f36..eb8147a636678 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -351,7 +351,7 @@ Expected count. * since: v1.20 * langs: python -The opposite of [`method: LocatorAssertions.toHaveCSS`]. +The opposite of [`method: LocatorAssertions.toHaveCSS#1`]. ### param: LocatorAssertions.NotToHaveCSS.name * since: v1.18 @@ -1694,7 +1694,7 @@ Expected count. ### option: LocatorAssertions.toHaveCount.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 -## async method: LocatorAssertions.toHaveCSS +## async method: LocatorAssertions.toHaveCSS#1 * since: v1.20 * langs: - alias-java: hasCSS @@ -1731,24 +1731,51 @@ var locator = Page.GetByRole(AriaRole.Button); await Expect(locator).ToHaveCSSAsync("display", "flex"); ``` -### param: LocatorAssertions.toHaveCSS.name +### param: LocatorAssertions.toHaveCSS#1.name * since: v1.18 - `name` <[string]> CSS property name. -### param: LocatorAssertions.toHaveCSS.value +### param: LocatorAssertions.toHaveCSS#1.value * since: v1.18 - `value` <[string]|[RegExp]> CSS property value. -### option: LocatorAssertions.toHaveCSS.timeout = %%-js-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS#1.timeout = %%-js-assertions-timeout-%% * since: v1.18 -### option: LocatorAssertions.toHaveCSS.timeout = %%-csharp-java-python-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS#1.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 da1add5293cf1..8ee02008a8fc4 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -3196,7 +3196,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) +- [`expect(locator).toHaveCSS(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-css-1) - [`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 114de1624a8cc..520a9419b87e3 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`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS#1`] | 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 c809dce782764..7baca2e707736 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) | Element has CSS property | +| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css-1) | 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 c0ac914bb2a30..1f45866bf5c5c 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,6 +1498,26 @@ 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') { @@ -1534,7 +1554,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; @@ -1546,7 +1566,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 11fbca82e978c..c381912a2c961 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1921,7 +1921,7 @@ scheme.FrameWaitForSelectorResult = tObject({ scheme.FrameExpectParams = tObject({ selector: tOptional(tString), expression: tString, - expressionArg: tOptional(tAny), + expressionArg: tOptional(tString), 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 a09b41d050cff..b32396d002855 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1480,8 +1480,11 @@ export class Frame extends SdkObject { lastIntermediateResult.received = received; } lastIntermediateResult.isSet = true; - if (!missingReceived && !Array.isArray(received)) - progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); + if (!missingReceived) { + const rendered = renderUnexpectedValue(options.expression, received); + if (rendered !== undefined) + progress.log(` unexpected value "${rendered}"`); + } } return { matches, received }; } @@ -1745,8 +1748,10 @@ function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.L return waitUntil; } -function renderUnexpectedValue(expression: string, received: any): string { +function renderUnexpectedValue(expression: string, received: any): string | undefined { if (expression === 'to.match.aria') - return received ? received.raw : received; - return received; + received = received?.raw; + if (Array.isArray(received) || (!!received && typeof received === 'object')) + return; + return String(received); } diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 08321af0c4cfe..f1f1e784a44a1 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -313,18 +313,30 @@ 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, - name: string, - expected: string | RegExp, - options?: { timeout?: number }, + arg1: string | Record, + arg2?: string | RegExp | ToHaveCSSOptions, + arg3?: ToHaveCSSOptions, ) { - 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); + 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)); + } } export function toHaveId( diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 9ed18fdf0eab1..011941effe469 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8396,221 +8396,60 @@ type FunctionAssertions = { toPass(options?: { timeout?: number, intervals?: number[] }): Promise; }; -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>; +type CSSStyleProperties = { [k in Exclude]?: CSSStyleDeclaration[k] extends Function ? never : CSSStyleDeclaration[k] }; /** - * 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. + * 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('navigates to login', async ({ page }) => { + * test('status becomes submitted', async ({ page }) => { * // ... - * const response = await page.request.get('https://playwright.dev'); - * await expect(response).toBeOK(); + * await page.getByRole('button').click(); + * await expect(page.locator('.status')).toHaveText('Submitted'); * }); * ``` * */ -interface APIResponseAssertions { +interface LocatorAssertions { /** - * Ensures the response status code is within `200..299` range. + * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) resolves to an element with the given computed + * CSS style. * * **Usage** * * ```js - * await expect(response).toBeOK(); + * const locator = page.getByRole('button'); + * await expect(locator).toHaveCSS('display', 'flex'); * ``` * + * @param name CSS property name. + * @param value CSS property value. + * @param options */ - toBeOK(): Promise; - + toHaveCSS(name: string, value: string|RegExp, options?: { timeout?: number }): Promise; /** - * Makes the assertion check for the opposite condition. For example, this code tests that the response status is not - * successful: + * 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. + * + * **Usage** * * ```js - * await expect(response).not.toBeOK(); + * const locator = page.getByRole('button'); + * await expect(locator).toHaveCSS({ + * display: 'flex', + * backgroundColor: 'rgb(255, 0, 0)' + * }); * ``` * + * @param styles CSS properties object. See + * [CSSStyleProperties](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleProperties) for available properties. + * @param options */ - 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 { + toHaveCSS(values: CSSStyleProperties, options?: { timeout?: number }): Promise; /** * 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. @@ -9163,28 +9002,6 @@ 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. @@ -9626,6 +9443,204 @@ 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. 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 2c9eb3345f188..69396233295ae 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3317,7 +3317,7 @@ export type FrameWaitForSelectorResult = { export type FrameExpectParams = { selector?: string, expression: string, - expressionArg?: any, + expressionArg?: string, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, @@ -3327,7 +3327,7 @@ export type FrameExpectParams = { }; export type FrameExpectOptions = { selector?: string, - expressionArg?: any, + expressionArg?: string, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index cc1a23646ab53..b3eccc0cfe67a 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2794,7 +2794,7 @@ Frame: parameters: selector: string? expression: string - expressionArg: json? + expressionArg: string? expectedText: type: array? items: ExpectedTextValue diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 16901c8be06f8..c5615c85147ee 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -518,6 +518,57 @@ 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 e71d6ebbf7790..bfcac9586a888 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -400,6 +400,13 @@ 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'>;