feat(api): introduce getByTestId (#17645)

This commit is contained in:
Pavel Feldman 2022-09-27 20:06:07 -08:00 committed by GitHub
parent 57337e8df8
commit d8f67eb75d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 205 additions and 10 deletions

View File

@ -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]>

View File

@ -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]>

View File

@ -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]>

View File

@ -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]>

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -303,6 +303,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel> 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));
}

View File

@ -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));
}

View File

@ -568,6 +568,10 @@ export class Page extends ChannelOwner<channels.PageChannel> 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);
}

View File

@ -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<SelectorsOwner>();
@ -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) {

View File

@ -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<void>;
/**
* 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;
}
/**

View File

@ -143,6 +143,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
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<TestFixtures, WorkerFixtures>({
_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);

View File

@ -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;
}

View File

@ -30,7 +30,7 @@ async function routeIframe(page: Page) {
body: `
<html>
<div>
<button>Hello iframe</button>
<button data-testid="buttonId">Hello iframe</button>
<iframe src="iframe-2.html"></iframe>
</div>
<span>1</span>
@ -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');
});

View File

@ -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('<div><div data-testid="Hello">Hello world</div></div>');
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');
});

View File

@ -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('<div data-pw="myid">Hi</div>');
await expect(page.getByTestId('myid')).toHaveCount(1);
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View File

@ -237,6 +237,7 @@ export interface PlaywrightTestOptions {
actionTimeout: number | undefined;
navigationTimeout: number | undefined;
serviceWorkers: ServiceWorkerPolicy | undefined;
testIdAttribute: string | undefined;
}