diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index ea1c0065da..7244592f6c 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -920,6 +920,16 @@ Attribute name to get the value for. * since: v1.8 +## method: Frame.getByLabelText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-label-text-%% + +### param: Frame.getByLabelText.text = %%-locator-get-by-text-text-%% +### option: Frame.getByLabelText.exact = %%-locator-get-by-text-exact-%% + + ## method: Frame.getByRole * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index d5d2f6dc3c..0cc0de2a0e 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -126,6 +126,16 @@ in that iframe. * since: v1.27 +## method: FrameLocator.getByLabelText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-label-text-%% + +### param: FrameLocator.getByLabelText.text = %%-locator-get-by-text-text-%% +### option: FrameLocator.getByLabelText.exact = %%-locator-get-by-text-exact-%% + + ## method: FrameLocator.getByRole * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index b09da96468..0f15c9fbc9 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -646,6 +646,16 @@ Attribute name to get the value for. * since: v1.14 +## method: Locator.getByLabelText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-label-text-%% + +### param: Locator.getByLabelText.text = %%-locator-get-by-text-text-%% +### option: Locator.getByLabelText.exact = %%-locator-get-by-text-exact-%% + + ## method: Locator.getByRole * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 33f32587d0..fa08796f06 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2195,6 +2195,16 @@ Attribute name to get the value for. * since: v1.8 +## method: Page.getByLabelText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-label-text-%% + +### param: Page.getByLabelText.text = %%-locator-get-by-text-text-%% +### option: Page.getByLabelText.exact = %%-locator-get-by-text-exact-%% + + ## method: Page.getByRole * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e4ec9849aa..29d2087e71 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1180,6 +1180,15 @@ Locate element by the test id. By default, the `data-testid` attribute is used a Allows locating elements that contain given text. +## template-locator-get-by-label-text + +Allows locating input elements by the text of the associated label. For example, this method will find the input by label text Password in the following DOM: + +```html + + +``` + ## template-locator-get-by-role Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). Note that role selector **does not replace** accessibility audits and conformance tests, but rather gives early feedback about the ARIA guidelines. diff --git a/docs/src/testing-library-js.md b/docs/src/testing-library-js.md index 33fc2f01a8..20c75b2a05 100644 --- a/docs/src/testing-library-js.md +++ b/docs/src/testing-library-js.md @@ -21,14 +21,14 @@ If you use DOM Testing Library in the browser (for example, you bundle end-to-en | [queries](https://testing-library.com/docs/queries/about) | [locators](./locators) | | [async helpers](https://testing-library.com/docs/dom-testing-library/api-async) | [assertions](./test-assertions) | | [user events](https://testing-library.com/docs/user-event/intro) | [actions](./api/class-locator) | -| `await user.click(screen.getByText('Click me'))` | `await component.locator('text=Click me').click()` | -| `await user.click(await screen.findByText('Click me'))` | `await component.locator('text=Click me').click()` | -| `await user.type(screen.getByLabelText('Password'), 'secret')` | `await component.locator('text=Password').fill('secret')` | -| `expect(screen.getByLabelText('Password')).toHaveValue('secret')` | `await expect(component.locator('text=Password')).toHaveValue('secret')` | -| `screen.findByText('...')` | `component.locator('text=...')` | -| `screen.getByTestId('...')` | `component.locator('data-testid=...')` | -| `screen.queryByPlaceholderText('...')` | `component.locator('[placeholder="..."]')` | -| `screen.getAllByRole('button', { pressed: true })` | `component.locator('role=button[pressed]')` | +| `await user.click(screen.getByText('Click me'))` | `await component.getByText('Click me').click()` | +| `await user.click(await screen.findByText('Click me'))` | `await component.getByText('Click me').click()` | +| `await user.type(screen.getByLabelText('Password'), 'secret')` | `await component.getByLabelText('Password').fill('secret')` | +| `expect(screen.getByLabelText('Password')).toHaveValue('secret')` | `await expect(component.getByLabelText('Password')).toHaveValue('secret')` | +| `screen.findByText('...')` | `component.getByText('...')` | +| `screen.getByTestId('...')` | `component.getByTestId('...')` | +| `screen.queryByPlaceholderText('...')` | `component.get('[placeholder="..."]')` | +| `screen.getByRole('button', { pressed: true })` | `component.getByRole('button', { pressed: true })`| ## Example diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index f39d290a41..b06910f478 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -307,6 +307,10 @@ export class Frame extends ChannelOwner implements api.Fr return this.locator(Locator.getByTestIdSelector(testId)); } + getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByLabelTextSelector(text, options)); + } + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.locator(Locator.getByTextSelector(text, options)); } diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index e51804a17d..e50f0af6e4 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -55,6 +55,14 @@ export class Locator implements api.Locator { return `css=[${Locator._testIdAttributeName}=${JSON.stringify(testId)}]`; } + static getByLabelTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { + if (!isString(text)) + return `text=${text}`; + const escaped = JSON.stringify(text); + const selector = options?.exact ? `text=${escaped}` : `text=${escaped.substring(1, escaped.length - 1)}`; + return selector + ' >> control=resolve-label'; + } + static getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { if (!isString(text)) return `text=${text}`; @@ -189,6 +197,10 @@ export class Locator implements api.Locator { return this.locator(Locator.getByTestIdSelector(testId)); } + getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByLabelTextSelector(text, options)); + } + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.locator(Locator.getByTextSelector(text, options)); } @@ -379,6 +391,9 @@ export class FrameLocator implements api.FrameLocator { return this.locator(Locator.getByTestIdSelector(testId)); } + getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByLabelTextSelector(text, options)); + } getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.locator(Locator.getByTextSelector(text, options)); } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 9fd0d8132f..ac27a28a50 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -572,6 +572,10 @@ export class Page extends ChannelOwner implements api.Page return this.mainFrame().getByTestId(testId); } + getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.mainFrame().getByLabelText(text, options); + } + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.mainFrame().getByText(text, options); } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index dcab9afe04..1aae3c635c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -278,6 +278,10 @@ export class InjectedScript { return []; if (body === 'return-empty') return []; + if (body === 'resolve-label') { + const control = (root as HTMLLabelElement).control; + return control ? [control] : []; + } if (body === 'component') { if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) return []; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 2ed03b59dc..b8b6892791 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2482,6 +2482,25 @@ export interface Page { timeout?: number; }): Promise; + /** + * Allows locating input elements by the text of the associated label. For example, this method will find the input by + * label text Password in the following DOM: + * + * ```html + * + * + * ``` + * + * @param text Text to locate the element for. + * @param options + */ + getByLabelText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ + exact?: boolean; + }): Locator; + /** * Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and @@ -5523,6 +5542,25 @@ export interface Frame { timeout?: number; }): Promise; + /** + * Allows locating input elements by the text of the associated label. For example, this method will find the input by + * label text Password in the following DOM: + * + * ```html + * + * + * ``` + * + * @param text Text to locate the element for. + * @param options + */ + getByLabelText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ + exact?: boolean; + }): Locator; + /** * Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and @@ -9911,6 +9949,25 @@ export interface Locator { timeout?: number; }): Promise; + /** + * Allows locating input elements by the text of the associated label. For example, this method will find the input by + * label text Password in the following DOM: + * + * ```html + * + * + * ``` + * + * @param text Text to locate the element for. + * @param options + */ + getByLabelText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ + exact?: boolean; + }): Locator; + /** * Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and @@ -15120,6 +15177,25 @@ export interface FrameLocator { hasText?: string|RegExp; }): Locator; + /** + * Allows locating input elements by the text of the associated label. For example, this method will find the input by + * label text Password in the following DOM: + * + * ```html + * + * + * ``` + * + * @param text Text to locate the element for. + * @param options + */ + getByLabelText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ + exact?: boolean; + }): Locator; + /** * Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts index de028e25dc..e2cf76902b 100644 --- a/tests/page/locator-frame.spec.ts +++ b/tests/page/locator-frame.spec.ts @@ -35,6 +35,7 @@ async function routeIframe(page: Page) { 1 2 + `, contentType: 'text/html' }).catch(() => {}); @@ -247,4 +248,6 @@ it('role and text coverage', async ({ page, server }) => { await expect(button1).toHaveText('Hello iframe'); await expect(button2).toHaveText('Hello iframe'); await expect(button3).toHaveText('Hello iframe'); + const input = page.frameLocator('iframe').getByLabelText('Name'); + await expect(input).toHaveValue(''); }); diff --git a/tests/page/selectors-text.spec.ts b/tests/page/selectors-text.spec.ts index ff57d6dcee..d79c3b1c67 100644 --- a/tests/page/selectors-text.spec.ts +++ b/tests/page/selectors-text.spec.ts @@ -454,3 +454,11 @@ it('should work with paired quotes in the middle of selector', async ({ page }) // Should double escape inside quoted text. await expect(page.locator(`div >> text='pattern "^-?\\\\d+$"'`)).toBeVisible(); }); + +it('getByLabelText should work', async ({ page, asset }) => { + await page.setContent(`
`); + expect(await page.getByText('Name').evaluate(e => e.nodeName)).toBe('LABEL'); + expect(await page.getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT'); + expect(await page.mainFrame().getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT'); + expect(await page.get('div').getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT'); +});