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
79 changes: 64 additions & 15 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
* since: v1.18

## async method: LocatorAssertions.NotToHaveAttribute
* since: v1.20
* since: v1.18
* langs: python

The opposite of [`method: LocatorAssertions.toHaveAttribute`].
The opposite of [`method: LocatorAssertions.toHaveAttribute#1`].

### param: LocatorAssertions.NotToHaveAttribute.name
* since: v1.18
Expand Down Expand Up @@ -885,21 +885,17 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
* since: v1.18


## async method: LocatorAssertions.toHaveAttribute
* since: v1.20
## async method: LocatorAssertions.toHaveAttribute#1
* since: v1.18
* langs:
- alias-java: hasAttribute

Ensures the [Locator] points to an element with given attribute. If the method
is used without `'value'` argument, then the method will assert attribute existance.
Ensures the [Locator] points to an element with given attribute value.

```js
const locator = page.locator('input');
// Assert attribute with given value.
await expect(locator).toHaveAttribute('type', 'text');
// Assert attribute existance.
await expect(locator).toHaveAttribute('disabled');
await expect(locator).not.toHaveAttribute('open');
```

```java
Expand All @@ -925,23 +921,76 @@ var locator = Page.Locator("input");
await Expect(locator).ToHaveAttributeAsync("type", "text");
```

### param: LocatorAssertions.toHaveAttribute.name
### param: LocatorAssertions.toHaveAttribute#1.name
* since: v1.18
- `name` <[string]>

Attribute name.

### param: LocatorAssertions.toHaveAttribute.value
### param: LocatorAssertions.toHaveAttribute#1.value
* since: v1.18
- `value` ?<[string]|[RegExp]>
- `value` <[string]|[RegExp]>

Optional expected attribute value. If missing, method will assert attribute presence.
Expected attribute value.

### option: LocatorAssertions.toHaveAttribute.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.toHaveAttribute#1.timeout = %%-js-assertions-timeout-%%
* since: v1.18
### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
### option: LocatorAssertions.toHaveAttribute#1.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18

## async method: LocatorAssertions.toHaveAttribute#2
* since: v1.26
* langs:
- alias-java: hasAttribute

Ensures the [Locator] points to an element with given attribute. The method will assert attribute
presence.

```js
const locator = page.locator('input');
// Assert attribute existance.
await expect(locator).toHaveAttribute('disabled');
await expect(locator).not.toHaveAttribute('open');
```

```java
assertThat(page.locator("input")).hasAttribute("disabled");
assertThat(page.locator("input")).not().hasAttribute("open");
```

```python async
from playwright.async_api import expect

locator = page.locator("input")
await expect(locator).to_have_attribute("disabled")
await expect(locator).not_to_have_attribute("open")
```

```python sync
from playwright.sync_api import expect

locator = page.locator("input")
expect(locator).to_have_attribute("disabled")
expect(locator).not_to_have_attribute("open")
```

```csharp
var locator = Page.Locator("input");
await Expect(locator).ToHaveAttributeAsync("disabled");
await Expect(locator).Not.ToHaveAttributeAsync("open");
```

### param: LocatorAssertions.toHaveAttribute#2.name
* since: v1.26
- `name` <[string]>

Attribute name.

### option: LocatorAssertions.toHaveAttribute#2.timeout = %%-js-assertions-timeout-%%
* since: v1.26
### option: LocatorAssertions.toHaveAttribute#2.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.26

## async method: LocatorAssertions.toHaveClass
* since: v1.20
* langs:
Expand Down
15 changes: 11 additions & 4 deletions packages/playwright-test/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import type { Locator, Page, APIResponse } from 'playwright-core';
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
import { colors } from 'playwright-core/lib/utilsBundle';
import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils';
import { constructURLBasedOnBaseURL, isRegExp } from 'playwright-core/lib/utils';
import type { Expect } from '../types';
import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy';
Expand Down Expand Up @@ -138,18 +138,25 @@ export function toHaveAttribute(
this: ReturnType<Expect['getState']>,
locator: LocatorEx,
name: string,
expected: string | RegExp | undefined,
expected: string | RegExp | undefined | { timeout?: number},
options?: { timeout?: number },
) {
if (!options) {
// Update params for the case toHaveAttribute(name, options);
if (typeof expected === 'object' && !isRegExp(expected)) {
options = expected;
expected = undefined;
}
}
if (expected === undefined) {
return toBeTruthy.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.have.attribute', { expressionArg: name, isNot, timeout });
}, options);
}
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
const expectedText = toExpectedTextValues([expected]);
const expectedText = toExpectedTextValues([expected as (string | RegExp)]);
return await locator._expect(customStackTrace, 'to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout });
}, expected, options);
}, expected as (string | RegExp), options);
}

export function toHaveClass(
Expand Down
24 changes: 20 additions & 4 deletions packages/playwright-test/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3423,23 +3423,39 @@ interface LocatorAssertions {
}): Promise<void>;

/**
* Ensures the [Locator] points to an element with given attribute. If the method is used without `'value'` argument, then
* the method will assert attribute existance.
* Ensures the [Locator] points to an element with given attribute value.
*
* ```js
* const locator = page.locator('input');
* // Assert attribute with given value.
* 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<void>;

/**
* Ensures the [Locator] points to an element with given attribute. The method will assert attribute presence.
*
* ```js
* const locator = page.locator('input');
* // Assert attribute existance.
* await expect(locator).toHaveAttribute('disabled');
* await expect(locator).not.toHaveAttribute('open');
* ```
*
* @param name Attribute name.
* @param value Optional expected attribute value. If missing, method will assert attribute presence.
* @param options
*/
toHaveAttribute(name: string, value?: string|RegExp, options?: {
toHaveAttribute(name: string, options?: {
/**
* Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`.
*/
Expand Down
9 changes: 9 additions & 0 deletions tests/page/expect-misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ test.describe('toHaveAttribute', () => {
await expect(locator).not.toHaveAttribute('open');
await expect(locator).toHaveAttribute('id', 'node');
});

test('should support boolean attribute with options', async ({ page }) => {
await page.setContent('<div checked id=node>Text content</div>');
const locator = page.locator('#node');
await expect(locator).toHaveAttribute('id', { timeout: 100 });
await expect(locator).toHaveAttribute('checked', { timeout: 100 });
await expect(locator).not.toHaveAttribute('open', { timeout: 100 });
await expect(locator).toHaveAttribute('id', 'node', { timeout: 100 });
});
});

test.describe('toHaveCSS', () => {
Expand Down
20 changes: 20 additions & 0 deletions tests/playwright-test/expect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,26 @@ test('should return void/Promise when appropriate', async ({ runTSC }) => {
expect(result.exitCode).toBe(0);
});

test('should suppport toHaveAttribute withou optional value', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
const { test } = pwt;
test('custom matchers', async ({ page }) => {
const locator = page.locator('#node');
await test.expect(locator).toHaveAttribute('name', 'value');
await test.expect(locator).toHaveAttribute('name', 'value', { timeout: 10 });
await test.expect(locator).toHaveAttribute('disabled');
await test.expect(locator).toHaveAttribute('disabled', { timeout: 10 });
// @ts-expect-error
await test.expect(locator).toHaveAttribute('disabled', { foo: 1 });
// @ts-expect-error
await test.expect(locator).toHaveAttribute('name', 'value', 'opt');
});
`
});
expect(result.exitCode).toBe(0);
});

test.describe('helpful expect errors', () => {
test('top-level', async ({ runInlineTest }) => {
const result = await runInlineTest({
Expand Down