diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 68e1d87c46a95..20c4378c04a35 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -890,11 +890,16 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev * langs: - alias-java: hasAttribute -Ensures the [Locator] points to an element with given attribute. +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. ```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 @@ -928,9 +933,9 @@ Attribute name. ### param: LocatorAssertions.toHaveAttribute.value * since: v1.18 -- `value` <[string]|[RegExp]> +- `value` ?<[string]|[RegExp]> -Expected attribute value. +Optional expected attribute value. If missing, method will assert attribute presence. ### option: LocatorAssertions.toHaveAttribute.timeout = %%-js-assertions-timeout-%% * since: v1.18 diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 312be0b8d29b8..7f81571ff9944 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1024,7 +1024,9 @@ export class InjectedScript { { // Element state / boolean values. let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined; - if (expression === 'to.be.checked') { + if (expression === 'to.have.attribute') { + elementState = element.hasAttribute(options.expressionArg); + } else if (expression === 'to.be.checked') { elementState = progress.injectedScript.elementState(element, 'checked'); } else if (expression === 'to.be.unchecked') { elementState = progress.injectedScript.elementState(element, 'unchecked'); @@ -1082,7 +1084,7 @@ export class InjectedScript { { // Single text value. let received: string | undefined; - if (expression === 'to.have.attribute') { + if (expression === 'to.have.attribute.value') { received = element.getAttribute(options.expressionArg) || ''; } else if (expression === 'to.have.class') { received = element.classList.toString(); diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index 2441058ca337e..b3aa9024ac74c 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -138,12 +138,17 @@ export function toHaveAttribute( this: ReturnType, locator: LocatorEx, name: string, - expected: string | RegExp, + expected: string | RegExp | undefined, options?: { timeout?: number }, ) { + 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]); - return await locator._expect(customStackTrace, 'to.have.attribute', { expressionArg: name, expectedText, isNot, timeout }); + return await locator._expect(customStackTrace, 'to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); }, expected, options); } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 5471fed48f8f2..18854276cdee1 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -3423,18 +3423,23 @@ interface LocatorAssertions { }): Promise; /** - * Ensures the [Locator] points to an element with given attribute. + * 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. * * ```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'); * ``` * * @param name Attribute name. - * @param value Expected attribute value. + * @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, value?: string|RegExp, 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 3b48c0d49e327..c3d09aac88600 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -228,8 +228,11 @@ test.describe('toHaveURL', () => { test.describe('toHaveAttribute', () => { test('pass', async ({ page }) => { - await page.setContent('
Text content
'); + await page.setContent('
Text content
'); const locator = page.locator('#node'); + await expect(locator).toHaveAttribute('id'); + await expect(locator).toHaveAttribute('checked'); + await expect(locator).not.toHaveAttribute('open'); await expect(locator).toHaveAttribute('id', 'node'); }); });