From d8f67eb75dab132a3b487b10c2451574515b17a5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Sep 2022 20:06:07 -0800 Subject: [PATCH] feat(api): introduce getByTestId (#17645) --- docs/src/api/class-frame.md | 10 +++ docs/src/api/class-framelocator.md | 10 +++ docs/src/api/class-locator.md | 10 +++ docs/src/api/class-page.md | 10 +++ docs/src/api/class-selectors.md | 11 +++ docs/src/api/params.md | 16 +++++ docs/src/test-api/class-testoptions.md | 5 ++ packages/playwright-core/src/client/frame.ts | 4 ++ .../playwright-core/src/client/locator.ts | 17 +++++ packages/playwright-core/src/client/page.ts | 4 ++ .../playwright-core/src/client/selectors.ts | 5 ++ packages/playwright-core/types/types.d.ts | 68 ++++++++++++++++--- packages/playwright-test/src/index.ts | 5 +- packages/playwright-test/types/test.d.ts | 6 ++ tests/page/locator-frame.spec.ts | 4 +- tests/page/selectors-css.spec.ts | 7 ++ .../playwright-test/playwright.config.spec.ts | 22 ++++++ utils/generate_types/overrides-test.d.ts | 1 + 18 files changed, 205 insertions(+), 10 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index c93540ce9b..ea1c0065da 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -932,6 +932,16 @@ Attribute name to get the value for. * since: v1.27 +## method: Frame.getByTestId +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-test-id-%% + +### param: Frame.getByTestId.testId = %%-locator-get-by-test-id-test-id-%% +* since: v1.27 + + ## method: Frame.getByText * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index ba89a6a405..d5d2f6dc3c 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -137,6 +137,16 @@ in that iframe. * since: v1.27 +## method: FrameLocator.getByTestId +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-test-id-%% + +### param: FrameLocator.getByTestId.testId = %%-locator-get-by-test-id-test-id-%% +* since: v1.27 + + ## method: FrameLocator.getByText * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index a179700674..b09da96468 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -657,6 +657,16 @@ Attribute name to get the value for. * since: v1.27 +## method: Locator.getByTestId +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-test-id-%% + +### param: Locator.getByTestId.testId = %%-locator-get-by-test-id-test-id-%% +* since: v1.27 + + ## method: Locator.getByText * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 0636b8d8eb..33f32587d0 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2206,6 +2206,16 @@ Attribute name to get the value for. * since: v1.27 +## method: Page.getByTestId +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-test-id-%% + +### param: Page.getByTestId.testId = %%-locator-get-by-test-id-test-id-%% +* since: v1.27 + + ## method: Page.getByText * since: v1.27 - returns: <[Locator]> diff --git a/docs/src/api/class-selectors.md b/docs/src/api/class-selectors.md index 397d3975a5..9b4705e1d3 100644 --- a/docs/src/api/class-selectors.md +++ b/docs/src/api/class-selectors.md @@ -214,3 +214,14 @@ Script that evaluates to a selector engine instance. The script is evaluated in Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but not any JavaScript objects from the frame's scripts. Defaults to `false`. Note that running as a content script is not guaranteed when this engine is used together with other registered engines. + +## method: Selectors.setTestIdAttribute +* since: v1.27 + +Defines custom attribute name to be used in [`method: Page.getByTestId`]. `data-testid` is used by default. + +### param: Selectors.setTestIdAttribute.attributeName +* since: v1.27 +- `attributeName` <[string]> + +Test id attribute name. diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 62463f0afa..e4ec9849aa 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1058,18 +1058,30 @@ When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, - %%-screenshot-option-mask-%% - %%-input-timeout-%% +## locator-get-by-test-id-test-id +* since: v1.27 +- `testId` <[string]> + +Id to locate the element by. + ## locator-get-by-text-text * since: v1.27 - `text` <[string]|[RegExp]> +Text to locate the element for. + ## locator-get-by-text-exact * since: v1.27 - `exact` <[boolean]> +Whether to find an exact match: case-sensitive and whole-string. Default to false. + ## locator-get-by-role-role * since: v1.27 - `role` <[string]> +Required aria role. + ## locator-get-by-role-option-checked * since: v1.27 - `checked` <[boolean]> @@ -1160,6 +1172,10 @@ Locator is resolved to the element immediately before performing an action, so a [Learn more about locators](../locators.md). +## template-locator-get-by-test-id + +Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use [`method: Selectors.setTestIdAttribute`] to configure a different test id attribute if necessary. + ## template-locator-get-by-text Allows locating elements that contain given text. diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 728a125715..17da609303 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -202,6 +202,11 @@ Learn more about [automatic screenshots](../test-configuration.md#automatic-scre ## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%% * since: v1.10 +## property: TestOptions.testIdAttribute +* since: v1.27 + +Custom attribute to be used in [`method: Page.getByTestId`]. `data-testid` is used by default. + ## property: TestOptions.timezoneId = %%-context-option-timezoneid-%% * since: v1.10 diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 886a7b5b64..f39d290a41 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -303,6 +303,10 @@ export class Frame extends ChannelOwner implements api.Fr return this.locator(selector, options); } + getByTestId(testId: string): Locator { + return this.locator(Locator.getByTestIdSelector(testId)); + } + 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 afd4143280..217686f93b 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -46,6 +46,15 @@ export class Locator implements api.Locator { _frame: Frame; _selector: string; + static _testIdAttributeName = 'data-testid'; + static _setTestIdAttribute(attributeName: string) { + Locator._testIdAttributeName = attributeName; + } + + static getByTestIdSelector(testId: string): string { + return `css=[${Locator._testIdAttributeName}=${testId}]`; + } + static getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { if (!isString(text)) return `text=${text}`; @@ -176,6 +185,10 @@ export class Locator implements api.Locator { return this.locator(selector, options); } + getByTestId(testId: string): Locator { + return this.locator(Locator.getByTestIdSelector(testId)); + } + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.locator(Locator.getByTextSelector(text, options)); } @@ -362,6 +375,10 @@ export class FrameLocator implements api.FrameLocator { return this.locator(selector, options); } + getByTestId(testId: string): Locator { + return this.locator(Locator.getByTestIdSelector(testId)); + } + 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 dc13cd37b6..9fd0d8132f 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -568,6 +568,10 @@ export class Page extends ChannelOwner implements api.Page return this.mainFrame().locator(selector, options); } + getByTestId(testId: string): Locator { + return this.mainFrame().getByTestId(testId); + } + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { return this.mainFrame().getByText(text, options); } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 68a562334b..77e83d6981 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -19,6 +19,7 @@ import type * as channels from '@protocol/channels'; import { ChannelOwner } from './channelOwner'; import type { SelectorEngine } from './types'; import type * as api from '../../types/types'; +import { Locator } from './locator'; export class Selectors implements api.Selectors { private _channels = new Set(); @@ -32,6 +33,10 @@ export class Selectors implements api.Selectors { this._registrations.push(params); } + setTestIdAttribute(attributeName: string) { + Locator._setTestIdAttribute(attributeName); + } + _addChannel(channel: SelectorsOwner) { this._channels.add(channel); for (const params of this._registrations) { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 81a831e48b..2ed03b59dc 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2492,7 +2492,7 @@ export interface Page { * [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You * can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not * recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values. - * @param role + * @param role Required aria role. * @param options */ getByRole(role: string, options?: { @@ -2557,12 +2557,23 @@ export interface Page { selected?: boolean; }): Locator; + /** + * Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use + * [selectors.setTestIdAttribute(attributeName)](https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute) + * to configure a different test id attribute if necessary. + * @param testId Id to locate the element by. + */ + getByTestId(testId: string): Locator; + /** * Allows locating elements that contain given text. - * @param text + * @param text Text to locate the element for. * @param options */ getByText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ exact?: boolean; }): Locator; @@ -5522,7 +5533,7 @@ export interface Frame { * [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You * can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not * recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values. - * @param role + * @param role Required aria role. * @param options */ getByRole(role: string, options?: { @@ -5587,12 +5598,23 @@ export interface Frame { selected?: boolean; }): Locator; + /** + * Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use + * [selectors.setTestIdAttribute(attributeName)](https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute) + * to configure a different test id attribute if necessary. + * @param testId Id to locate the element by. + */ + getByTestId(testId: string): Locator; + /** * Allows locating elements that contain given text. - * @param text + * @param text Text to locate the element for. * @param options */ getByText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ exact?: boolean; }): Locator; @@ -9899,7 +9921,7 @@ export interface Locator { * [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You * can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not * recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values. - * @param role + * @param role Required aria role. * @param options */ getByRole(role: string, options?: { @@ -9964,12 +9986,23 @@ export interface Locator { selected?: boolean; }): Locator; + /** + * Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use + * [selectors.setTestIdAttribute(attributeName)](https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute) + * to configure a different test id attribute if necessary. + * @param testId Id to locate the element by. + */ + getByTestId(testId: string): Locator; + /** * Allows locating elements that contain given text. - * @param text + * @param text Text to locate the element for. * @param options */ getByText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ exact?: boolean; }): Locator; @@ -15097,7 +15130,7 @@ export interface FrameLocator { * [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You * can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not * recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values. - * @param role + * @param role Required aria role. * @param options */ getByRole(role: string, options?: { @@ -15162,12 +15195,23 @@ export interface FrameLocator { selected?: boolean; }): Locator; + /** + * Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use + * [selectors.setTestIdAttribute(attributeName)](https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute) + * to configure a different test id attribute if necessary. + * @param testId Id to locate the element by. + */ + getByTestId(testId: string): Locator; + /** * Allows locating elements that contain given text. - * @param text + * @param text Text to locate the element for. * @param options */ getByText(text: string|RegExp, options?: { + /** + * Whether to find an exact match: case-sensitive and whole-string. Default to false. + */ exact?: boolean; }): Locator; @@ -16252,6 +16296,14 @@ export interface Selectors { */ contentScript?: boolean; }): Promise; + + /** + * Defines custom attribute name to be used in + * [page.getByTestId(testId)](https://playwright.dev/docs/api/class-page#page-get-by-test-id). `data-testid` is used by + * default. + * @param attributeName Test id attribute name. + */ + setTestIdAttribute(attributeName: string): void; } /** diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index ba1b860550..1e7b82bdde 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -143,6 +143,7 @@ export const test = _baseTest.extend({ userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }], viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }], actionTimeout: [0, { option: true }], + testIdAttribute: ['data-testid', { option: true }], navigationTimeout: [0, { option: true }], baseURL: [async ({ }, use) => { await use(process.env.PLAYWRIGHT_TEST_BASE_URL); @@ -225,7 +226,9 @@ export const test = _baseTest.extend({ _snapshotSuffix: [process.platform, { scope: 'worker' }], - _setupContextOptionsAndArtifacts: [async ({ playwright, _snapshotSuffix, _combinedContextOptions, _browserOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => { + _setupContextOptionsAndArtifacts: [async ({ playwright, _snapshotSuffix, _combinedContextOptions, _browserOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => { + if (testIdAttribute) + playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute); testInfo.snapshotSuffix = _snapshotSuffix; if (debugMode()) testInfo.setTimeout(0); diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 47f073679d..3321513921 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -2969,6 +2969,12 @@ export interface PlaywrightTestOptions { * - `'block'`: Playwright will block all registration of Service Workers. */ serviceWorkers: ServiceWorkerPolicy | undefined; + /** + * Custom attribute to be used in + * [page.getByTestId(testId)](https://playwright.dev/docs/api/class-page#page-get-by-test-id). `data-testid` is used by + * default. + */ + testIdAttribute: string | undefined; } diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts index e080cbbd72..de028e25dc 100644 --- a/tests/page/locator-frame.spec.ts +++ b/tests/page/locator-frame.spec.ts @@ -30,7 +30,7 @@ async function routeIframe(page: Page) { body: `
- +
1 @@ -243,6 +243,8 @@ it('role and text coverage', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); const button1 = page.frameLocator('iframe').getByRole('button'); const button2 = page.frameLocator('iframe').getByText('Hello'); + const button3 = page.frameLocator('iframe').getByTestId('buttonId'); await expect(button1).toHaveText('Hello iframe'); await expect(button2).toHaveText('Hello iframe'); + await expect(button3).toHaveText('Hello iframe'); }); diff --git a/tests/page/selectors-css.spec.ts b/tests/page/selectors-css.spec.ts index 37d09afc08..c9bf2915ef 100644 --- a/tests/page/selectors-css.spec.ts +++ b/tests/page/selectors-css.spec.ts @@ -413,3 +413,10 @@ it('css on the handle should be relative', async ({ page }) => { expect(await div.$eval(`.find-me`, e => e.id)).toBe('target2'); expect(await page.$eval(`div >> .find-me`, e => e.id)).toBe('target2'); }); + +it('getByTestId should work', async ({ page }) => { + await page.setContent('
Hello world
'); + await expect(page.getByTestId('Hello')).toHaveText('Hello world'); + await expect(page.mainFrame().getByTestId('Hello')).toHaveText('Hello world'); + await expect(page.get('div').getByTestId('Hello')).toHaveText('Hello world'); +}); diff --git a/tests/playwright-test/playwright.config.spec.ts b/tests/playwright-test/playwright.config.spec.ts index 4f08723b0b..90e6e699e6 100644 --- a/tests/playwright-test/playwright.config.spec.ts +++ b/tests/playwright-test/playwright.config.spec.ts @@ -184,3 +184,25 @@ test('should override contextOptions', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); }); + +test('should respect testIdAttribute', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { + testIdAttribute: 'data-pw', + } + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async ({ page }) => { + await page.setContent('
Hi
'); + await expect(page.getByTestId('myid')).toHaveCount(1); + }); + `, + }, { workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index b8048028da..8d85557b70 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -237,6 +237,7 @@ export interface PlaywrightTestOptions { actionTimeout: number | undefined; navigationTimeout: number | undefined; serviceWorkers: ServiceWorkerPolicy | undefined; + testIdAttribute: string | undefined; }