diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index ad06c25142..4124594c5a 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -211,11 +211,8 @@ The opposite of [`method: LocatorAssertions.toContainText`]. Expected substring or RegExp or a list of those. -### option: LocatorAssertions.NotToContainText.ignoreCase +### option: LocatorAssertions.NotToContainText.ignoreCase = %%-assertions-ignore-case-%% * since: v1.23 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ### option: LocatorAssertions.NotToContainText.useInnerText * since: v1.18 @@ -226,6 +223,26 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev ### option: LocatorAssertions.NotToContainText.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 + +## async method: LocatorAssertions.NotToHaveAccessibleName +* since: v1.44 +* langs: python + +The opposite of [`method: LocatorAssertions.toHaveAccessibleName`]. + +### param: LocatorAssertions.NotToHaveAccessibleName.name +* since: v1.44 +- `name` <[string]|[RegExp]> + +Expected accessible name. + +### option: LocatorAssertions.NotToHaveAccessibleName.ignoreCase = %%-assertions-ignore-case-%% +* since: v1.44 + +### option: LocatorAssertions.NotToHaveAccessibleName.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.44 + + ## async method: LocatorAssertions.NotToHaveAttribute * since: v1.20 * langs: python @@ -244,11 +261,8 @@ Attribute name. Expected attribute value. -### option: LocatorAssertions.NotToHaveAttribute.ignoreCase +### option: LocatorAssertions.NotToHaveAttribute.ignoreCase = %%-assertions-ignore-case-%% * since: v1.40 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ### option: LocatorAssertions.NotToHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 @@ -352,11 +366,8 @@ The opposite of [`method: LocatorAssertions.toHaveText`]. Expected string or RegExp or a list of those. -### option: LocatorAssertions.NotToHaveText.ignoreCase +### option: LocatorAssertions.NotToHaveText.ignoreCase = %%-assertions-ignore-case-%% * since: v1.23 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ### option: LocatorAssertions.NotToHaveText.useInnerText * since: v1.18 @@ -1089,11 +1100,8 @@ Expected substring or RegExp or a list of those. Expected substring or RegExp or a list of those. -### option: LocatorAssertions.toContainText.ignoreCase +### option: LocatorAssertions.toContainText.ignoreCase = %%-assertions-ignore-case-%% * since: v1.23 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ### option: LocatorAssertions.toContainText.useInnerText * since: v1.18 @@ -1107,6 +1115,57 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev ### option: LocatorAssertions.toContainText.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 + +## async method: LocatorAssertions.toHaveAccessibleName +* since: v1.44 +* langs: + - alias-java: hasAccessibleName + +Ensures the [Locator] points to an element with a given [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + +**Usage** + +```js +const locator = page.getByTestId('save-button'); +await expect(locator).toHaveAccessibleName('Save to disk'); +``` + +```java +Locator locator = page.getByTestId("save-button"); +assertThat(locator).hasAccessibleName("Save to disk"); +``` + +```python async +locator = page.get_by_test_id("save-button") +await expect(locator).to_have_accessible_name("Save to disk") +``` + +```python sync +locator = page.get_by_test_id("save-button") +expect(locator).to_have_accessible_name("Save to disk") +``` + +```csharp +var locator = Page.GetByTestId("save-button"); +await Expect(locator).toHaveAccessibleNameAsync("Save to disk"); +``` + +### param: LocatorAssertions.toHaveAccessibleName.name +* since: v1.44 +- `name` <[string]|[RegExp]> + +Expected accessible name. + +### option: LocatorAssertions.toHaveAccessibleName.timeout = %%-js-assertions-timeout-%% +* since: v1.44 + +### option: LocatorAssertions.toHaveAccessibleName.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.44 + +### option: LocatorAssertions.toHaveAccessibleName.ignoreCase = %%-assertions-ignore-case-%% +* since: v1.44 + + ## async method: LocatorAssertions.toHaveAttribute * since: v1.20 * langs: @@ -1162,11 +1221,8 @@ Expected attribute value. ### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 -### option: LocatorAssertions.toHaveAttribute.ignoreCase +### option: LocatorAssertions.toHaveAttribute.ignoreCase = %%-assertions-ignore-case-%% * since: v1.40 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ## async method: LocatorAssertions.toHaveAttribute#2 * since: v1.39 @@ -1769,11 +1825,8 @@ Expected string or RegExp or a list of those. Expected string or RegExp or a list of those. -### option: LocatorAssertions.toHaveText.ignoreCase +### option: LocatorAssertions.toHaveText.ignoreCase = %%-assertions-ignore-case-%% * since: v1.23 -- `ignoreCase` <[boolean]> - -Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. ### option: LocatorAssertions.toHaveText.useInnerText * since: v1.18 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e3b2894c3c..287fbeef9c 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -846,6 +846,11 @@ Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestC Time to retry the assertion for in milliseconds. Defaults to `5000`. +## assertions-ignore-case +- `ignoreCase` <[boolean]> + +Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. + ## assertions-max-diff-pixels * langs: js - `maxDiffPixels` <[int]> diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 37ed8cbc94..890b7f3e7c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1223,6 +1223,8 @@ export class InjectedScript { received = element.id; } else if (expression === 'to.have.text') { received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new Map(), element).full; + } else if (expression === 'to.have.accessible.name') { + received = getElementAccessibleName(element, false /* includeHidden */); } else if (expression === 'to.have.title') { received = this.document.title; } else if (expression === 'to.have.url') { diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index ef521ffc44..9145086b26 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -32,6 +32,7 @@ import { toBeOK, toBeVisible, toContainText, + toHaveAccessibleName, toHaveAttribute, toHaveClass, toHaveCount, @@ -185,6 +186,7 @@ const customAsyncMatchers = { toBeOK, toBeVisible, toContainText, + toHaveAccessibleName, toHaveAttribute, toHaveClass, toHaveCount, diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 1e3bdf9a45..d6590ce11d 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -174,6 +174,18 @@ export function toContainText( } } +export function toHaveAccessibleName( + this: ExpectMatcherContext, + locator: LocatorEx, + expected: string | RegExp, + options?: { timeout?: number, ignoreCase?: boolean }, +) { + return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { + const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); + }, expected, options); +} + export function toHaveAttribute( this: ExpectMatcherContext, locator: LocatorEx, diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 42d85d45fc..cfdccad022 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6882,6 +6882,33 @@ interface LocatorAssertions { useInnerText?: boolean; }): Promise; + /** + * Ensures the {@link Locator} points to an element with a given + * [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + * + * **Usage** + * + * ```js + * const locator = page.getByTestId('save-button'); + * await expect(locator).toHaveAccessibleName('Save to disk'); + * ``` + * + * @param name Expected accessible name. + * @param options + */ + toHaveAccessibleName(name: string|RegExp, options?: { + /** + * Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + * expression flag if specified. + */ + ignoreCase?: boolean; + + /** + * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * Ensures the {@link Locator} points to an element with given attribute. * diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 3d09341468..8f5bf81201 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -419,3 +419,15 @@ test('toHaveText that does not match should not produce logs twice', async ({ pa expect(error.message).not.toContain('locator resolved to'); expect(error.message.replace(waitingForMessage, '')).not.toContain(waitingForMessage); }); + +test('toHaveAccessibleName', async ({ page }) => { + await page.setContent(` +
+ `); + await expect(page.locator('div')).toHaveAccessibleName('Hello'); + await expect(page.locator('div')).not.toHaveAccessibleName('hello'); + await expect(page.locator('div')).toHaveAccessibleName('hello', { ignoreCase: true }); + await expect(page.locator('div')).toHaveAccessibleName(/ell\w/); + await expect(page.locator('div')).not.toHaveAccessibleName(/hello/); + await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true }); +});