diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 20c4378c04..2fd7927184 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -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 @@ -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 @@ -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: diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index b3aa9024ac..b191930508 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -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'; @@ -138,18 +138,25 @@ export function toHaveAttribute( this: ReturnType, 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( diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index bfa7f06a82..6e7b3b6ae6 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -3423,23 +3423,39 @@ interface LocatorAssertions { }): Promise; /** - * 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; + + /** + * 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`. */ diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index c3d09aac88..c32d971f3f 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -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('
Text content
'); + 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', () => { diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index bc5c3c6a11..24dccba74a 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -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({