diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index fe757d123e..7b6eba3d9e 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -1099,7 +1099,10 @@ export class InjectedScript {
// Single text value.
let received: string | undefined;
if (expression === 'to.have.attribute') {
- received = element.getAttribute(options.expressionArg) || '';
+ const value = element.getAttribute(options.expressionArg);
+ if (value === null)
+ return { received: null, matches: false };
+ received = value;
} else if (expression === 'to.have.class') {
received = element.classList.toString();
} else if (expression === 'to.have.css') {
diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts
index 3b48c0d49e..87f900295f 100644
--- a/tests/page/expect-misc.spec.ts
+++ b/tests/page/expect-misc.spec.ts
@@ -232,6 +232,36 @@ test.describe('toHaveAttribute', () => {
const locator = page.locator('#node');
await expect(locator).toHaveAttribute('id', 'node');
});
+
+ test('should not match missing attribute', async ({ page }) => {
+ await page.setContent('
Text content
');
+ const locator = page.locator('#node');
+ {
+ const error = await expect(locator).toHaveAttribute('disabled', '', { timeout: 1000 }).catch(e => e);
+ expect(error.message).toContain('expect.toHaveAttribute with timeout 1000ms');
+ }
+ {
+ const error = await expect(locator).toHaveAttribute('disabled', /.*/, { timeout: 1000 }).catch(e => e);
+ expect(error.message).toContain('expect.toHaveAttribute with timeout 1000ms');
+ }
+ await expect(locator).not.toHaveAttribute('disabled', '');
+ await expect(locator).not.toHaveAttribute('disabled', /.*/);
+ });
+
+ test('should match boolean attribute', async ({ page }) => {
+ await page.setContent('Text content
');
+ const locator = page.locator('#node');
+ await expect(locator).toHaveAttribute('checked', '');
+ await expect(locator).toHaveAttribute('checked', /.*/);
+ {
+ const error = await expect(locator).not.toHaveAttribute('checked', '', { timeout: 1000 }).catch(e => e);
+ expect(error.message).toContain('expect.toHaveAttribute with timeout 1000ms');
+ }
+ {
+ const error = await expect(locator).not.toHaveAttribute('checked', /.*/, { timeout: 1000 }).catch(e => e);
+ expect(error.message).toContain('expect.toHaveAttribute with timeout 1000ms');
+ }
+ });
});
test.describe('toHaveCSS', () => {