Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/actionability.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
39 changes: 33 additions & 6 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/release-notes-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-assertions-csharp-java-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-assertions-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
28 changes: 24 additions & 4 deletions packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 };
Expand All @@ -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<string, string>;
const received: Record<string, string> = {};
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') {
Expand Down Expand Up @@ -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;
Expand All @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down
15 changes: 10 additions & 5 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1480,8 +1480,11 @@ export class Frame extends SdkObject<FrameEventMap> {
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}"`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, why don't we log arrays/objects?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They end up as [Object object]. We can probably work on making this better, but separately.

}
}
return { matches, received };
}
Expand Down Expand Up @@ -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);
}
26 changes: 19 additions & 7 deletions packages/playwright/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MatcherResult<any, any>>;
export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, styles: Record<string, string>, options?: { timeout?: number }): Promise<MatcherResult<any, any>>;
export function toHaveCSS(
this: ExpectMatcherStateInternal,
locator: LocatorEx,
name: string,
expected: string | RegExp,
options?: { timeout?: number },
arg1: string | Record<string, string>,
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(
Expand Down
Loading
Loading