mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
feat: toHaveAttribute without value (#27418)
This time not doing it in other languages due to unjustified generator complexity. Fixes #27341
This commit is contained in:
parent
5295d468ad
commit
ac48a47d33
@ -1151,6 +1151,29 @@ Expected attribute value.
|
|||||||
### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
|
### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||||
* since: v1.18
|
* since: v1.18
|
||||||
|
|
||||||
|
## async method: LocatorAssertions.toHaveAttribute#2
|
||||||
|
* since: v1.40
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Ensures the [Locator] points to an element with given attribute. The method will assert attribute
|
||||||
|
presence.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const locator = page.locator('input');
|
||||||
|
// Assert attribute existence.
|
||||||
|
await expect(locator).toHaveAttribute('disabled');
|
||||||
|
await expect(locator).not.toHaveAttribute('open');
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: LocatorAssertions.toHaveAttribute#2.name
|
||||||
|
* since: v1.40
|
||||||
|
- `name` <[string]>
|
||||||
|
|
||||||
|
Attribute name.
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveAttribute#2.timeout = %%-js-assertions-timeout-%%
|
||||||
|
* since: v1.40
|
||||||
|
|
||||||
## async method: LocatorAssertions.toHaveClass
|
## async method: LocatorAssertions.toHaveClass
|
||||||
* since: v1.20
|
* since: v1.20
|
||||||
* langs:
|
* langs:
|
||||||
|
@ -1206,7 +1206,9 @@ export class InjectedScript {
|
|||||||
{
|
{
|
||||||
// Element state / boolean values.
|
// Element state / boolean values.
|
||||||
let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined;
|
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 = this.elementState(element, 'checked');
|
elementState = this.elementState(element, 'checked');
|
||||||
} else if (expression === 'to.be.unchecked') {
|
} else if (expression === 'to.be.unchecked') {
|
||||||
elementState = this.elementState(element, 'unchecked');
|
elementState = this.elementState(element, 'unchecked');
|
||||||
@ -1277,7 +1279,7 @@ export class InjectedScript {
|
|||||||
{
|
{
|
||||||
// Single text value.
|
// Single text value.
|
||||||
let received: string | undefined;
|
let received: string | undefined;
|
||||||
if (expression === 'to.have.attribute') {
|
if (expression === 'to.have.attribute.value') {
|
||||||
const value = element.getAttribute(options.expressionArg);
|
const value = element.getAttribute(options.expressionArg);
|
||||||
if (value === null)
|
if (value === null)
|
||||||
return { received: null, matches: false };
|
return { received: null, matches: false };
|
||||||
|
@ -21,7 +21,7 @@ import { expectTypes, callLogText, filteredStackTrace } from '../util';
|
|||||||
import { toBeTruthy } from './toBeTruthy';
|
import { toBeTruthy } from './toBeTruthy';
|
||||||
import { toEqual } from './toEqual';
|
import { toEqual } from './toEqual';
|
||||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||||
import { captureRawStack, constructURLBasedOnBaseURL, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
|
import { captureRawStack, constructURLBasedOnBaseURL, isRegExp, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo } from '../common/globals';
|
||||||
import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo';
|
import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo';
|
||||||
import type { ExpectMatcherContext } from './expect';
|
import type { ExpectMatcherContext } from './expect';
|
||||||
@ -177,13 +177,25 @@ export function toHaveAttribute(
|
|||||||
this: ExpectMatcherContext,
|
this: ExpectMatcherContext,
|
||||||
locator: LocatorEx,
|
locator: LocatorEx,
|
||||||
name: string,
|
name: string,
|
||||||
expected: string | RegExp,
|
expected: string | RegExp | undefined | { timeout?: number },
|
||||||
options?: { 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', 'have attribute', 'not have attribute', '', async (isNot, timeout) => {
|
||||||
|
return await locator._expect('to.have.attribute', { expressionArg: name, isNot, timeout });
|
||||||
|
}, options);
|
||||||
|
}
|
||||||
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => {
|
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => {
|
||||||
const expectedText = toExpectedTextValues([expected]);
|
const expectedText = toExpectedTextValues([expected as (string | RegExp)]);
|
||||||
return await locator._expect('to.have.attribute', { expressionArg: name, expectedText, isNot, timeout });
|
return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout });
|
||||||
}, expected, options);
|
}, expected as (string | RegExp), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toHaveClass(
|
export function toHaveClass(
|
||||||
|
20
packages/playwright/types/test.d.ts
vendored
20
packages/playwright/types/test.d.ts
vendored
@ -5583,6 +5583,26 @@ interface LocatorAssertions {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the {@link Locator} points to an element with given attribute. The method will assert attribute presence.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const locator = page.locator('input');
|
||||||
|
* // Assert attribute existence.
|
||||||
|
* await expect(locator).toHaveAttribute('disabled');
|
||||||
|
* await expect(locator).not.toHaveAttribute('open');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param name Attribute name.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
toHaveAttribute(name: string, options?: {
|
||||||
|
/**
|
||||||
|
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the {@link Locator} points to an element with given CSS classes. This needs to be a full match or using a
|
* Ensures the {@link Locator} points to an element with given CSS classes. This needs to be a full match or using a
|
||||||
* relaxed regular expression.
|
* relaxed regular expression.
|
||||||
|
@ -262,6 +262,22 @@ test.describe('toHaveAttribute', () => {
|
|||||||
expect(error.message).toContain('expect.not.toHaveAttribute with timeout 1000ms');
|
expect(error.message).toContain('expect.not.toHaveAttribute with timeout 1000ms');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should match attribute without value', async ({ page }) => {
|
||||||
|
await page.setContent('<div checked id=node>Text content</div>');
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await expect(locator).toHaveAttribute('id');
|
||||||
|
await expect(locator).toHaveAttribute('checked');
|
||||||
|
await expect(locator).not.toHaveAttribute('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 5000 });
|
||||||
|
await expect(locator).toHaveAttribute('checked', { timeout: 5000 });
|
||||||
|
await expect(locator).not.toHaveAttribute('open', { timeout: 5000 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('toHaveCSS', () => {
|
test.describe('toHaveCSS', () => {
|
||||||
|
@ -859,3 +859,23 @@ test('should chain expect matchers and expose matcher utils', async ({ runInline
|
|||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should suppport toHaveAttribute without optional value', async ({ runTSC }) => {
|
||||||
|
const result = await runTSC({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect as baseExpect } from '@playwright/test';
|
||||||
|
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);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user