From d9a28bd2442fdf7c4d46fa07778241851fb53b9c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Sep 2022 15:13:56 -0800 Subject: [PATCH] feat(api): introduce get/getByText/getByRole (#17577) --- docs/src/api/class-frame.md | 38 +- docs/src/api/class-framelocator.md | 33 ++ docs/src/api/class-locator.md | 34 ++ docs/src/api/class-page.md | 41 +- docs/src/api/params.md | 109 +++++ packages/html-reporter/src/chip.spec.tsx | 14 +- .../html-reporter/src/headerView.spec.tsx | 20 +- .../html-reporter/src/imageDiffView.spec.tsx | 20 +- .../html-reporter/src/testCaseView.spec.tsx | 18 +- packages/playwright-core/src/client/frame.ts | 15 +- .../playwright-core/src/client/locator.ts | 66 ++- packages/playwright-core/src/client/page.ts | 14 +- packages/playwright-core/types/types.d.ts | 459 +++++++++++++++++- tests/library/trace-viewer.spec.ts | 46 +- tests/page/locator-frame.spec.ts | 19 +- tests/page/locator-query.spec.ts | 8 + tests/page/selectors-role.spec.ts | 157 ++++-- tests/page/selectors-text.spec.ts | 2 + 18 files changed, 989 insertions(+), 124 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index ddf5d09030..c93540ce9b 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -887,6 +887,18 @@ await locator.ClickAsync(); * since: v1.17 +## method: Frame.get +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-root-locator-%% + +### param: Frame.get.selector = %%-find-selector-%% +* since: v1.27 +### option: Frame.get.-inline- = %%-locator-options-list-v1.14-%% +* since: v1.27 + + ## async method: Frame.getAttribute * since: v1.8 - returns: <[null]|[string]> @@ -907,6 +919,29 @@ Attribute name to get the value for. ### option: Frame.getAttribute.timeout = %%-input-timeout-%% * since: v1.8 + +## method: Frame.getByRole +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-role-%% + + +### param: Frame.getByRole.role = %%-locator-get-by-role-role-%% +### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%% +* since: v1.27 + + +## method: Frame.getByText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-text-%% + +### param: Frame.getByText.text = %%-locator-get-by-text-text-%% +### option: Frame.getByText.exact = %%-locator-get-by-text-exact-%% + + ## async method: Frame.goto * since: v1.8 * langs: @@ -1131,8 +1166,7 @@ Returns whether the element is [visible](../actionability.md#visible). [`option: * since: v1.14 - returns: <[Locator]> -The method returns an element locator that can be used to perform actions in the frame. -Locator is resolved to the element immediately before performing an action, so a series of actions on the same locator can in fact be performed on different DOM elements. That would happen if the DOM structure between those actions has changed. +%%-template-locator-root-locator-%% [Learn more about locators](../locators.md). diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index e39cd27fcb..ba89a6a405 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -114,6 +114,39 @@ in that iframe. * since: v1.17 +## method: FrameLocator.get +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-locator-%% + +### param: FrameLocator.get.selector = %%-find-selector-%% +* since: v1.27 +### option: FrameLocator.get.-inline- = %%-locator-options-list-v1.14-%% +* since: v1.27 + + +## method: FrameLocator.getByRole +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-role-%% + +### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%% +### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%% +* since: v1.27 + + +## method: FrameLocator.getByText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-text-%% + +### param: FrameLocator.getByText.text = %%-locator-get-by-text-text-%% +### option: FrameLocator.getByText.exact = %%-locator-get-by-text-exact-%% + + ## method: FrameLocator.last * since: v1.17 - returns: <[FrameLocator]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 691a78cf50..a179700674 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -618,6 +618,18 @@ await locator.ClickAsync(); * since: v1.17 +## method: Locator.get +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-locator-%% + +### param: Locator.get.selector = %%-find-selector-%% +* since: v1.27 +### option: Locator.get.-inline- = %%-locator-options-list-v1.14-%% +* since: v1.27 + + ## async method: Locator.getAttribute * since: v1.14 - returns: <[null]|[string]> @@ -633,6 +645,28 @@ Attribute name to get the value for. ### option: Locator.getAttribute.timeout = %%-input-timeout-%% * since: v1.14 + +## method: Locator.getByRole +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-role-%% + +### param: Locator.getByRole.role = %%-locator-get-by-role-role-%% +### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%% +* since: v1.27 + + +## method: Locator.getByText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-text-%% + +### param: Locator.getByText.text = %%-locator-get-by-text-text-%% +### option: Locator.getByText.exact = %%-locator-get-by-text-exact-%% + + ## async method: Locator.highlight * since: v1.20 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index c375d17e77..0636b8d8eb 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2162,6 +2162,18 @@ await locator.ClickAsync(); An array of all frames attached to the page. +## method: Page.get +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-root-locator-%% + +### param: Page.get.selector = %%-find-selector-%% +* since: v1.27 +### option: Page.get.-inline- = %%-locator-options-list-v1.14-%% +* since: v1.27 + + ## async method: Page.getAttribute * since: v1.8 - returns: <[null]|[string]> @@ -2182,6 +2194,28 @@ Attribute name to get the value for. ### option: Page.getAttribute.timeout = %%-input-timeout-%% * since: v1.8 + +## method: Page.getByRole +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-role-%% + +### param: Page.getByRole.role = %%-locator-get-by-role-role-%% +### option: Page.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%% +* since: v1.27 + + +## method: Page.getByText +* since: v1.27 +- returns: <[Locator]> + +%%-template-locator-get-by-text-%% + +### param: Page.getByText.text = %%-locator-get-by-text-text-%% +### option: Page.getByText.exact = %%-locator-get-by-text-exact-%% + + ## async method: Page.goBack * since: v1.8 - returns: <[null]|[Response]> @@ -2447,12 +2481,7 @@ Returns whether the element is [visible](../actionability.md#visible). [`option: * since: v1.14 - returns: <[Locator]> -The method returns an element locator that can be used to perform actions on the page. -Locator is resolved to the element immediately before performing an action, so a series of actions on the same locator can in fact be performed on different DOM elements. That would happen if the DOM structure between those actions has changed. - -[Learn more about locators](../locators.md). - -Shortcut for main frame's [`method: Frame.locator`]. +%%-template-locator-root-locator-%% ### param: Page.locator.selector = %%-find-selector-%% * since: v1.14 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 26a20e6257..62463f0afa 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1058,6 +1058,115 @@ When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, - %%-screenshot-option-mask-%% - %%-input-timeout-%% +## locator-get-by-text-text +* since: v1.27 +- `text` <[string]|[RegExp]> + +## locator-get-by-text-exact +* since: v1.27 +- `exact` <[boolean]> + +## locator-get-by-role-role +* since: v1.27 +- `role` <[string]> + +## locator-get-by-role-option-checked +* since: v1.27 +- `checked` <[boolean]> + +An attribute that is usually set by `aria-checked` or native `` controls. Available values for checked are `true`, `false` and `"mixed"`. + +Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked). + +## locator-get-by-role-option-disabled +* since: v1.27 +- `disabled` <[boolean]> + +A boolean attribute that is usually set by `aria-disabled` or `disabled`. + +:::note +Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. +Learn more about [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled). +::: + +## locator-get-by-role-option-expanded +* since: v1.27 +- `expanded` <[boolean]> + +A boolean attribute that is usually set by `aria-expanded`. + + Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). + +## locator-get-by-role-option-includeHidden +* since: v1.27 +- `includeHidden` <[boolean]> + +A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector. + +Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden). + +## locator-get-by-role-option-level +* since: v1.27 +- `level` <[int]> + +A number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for `

-

` elements. + +Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level). + +## locator-get-by-role-option-name +* since: v1.27 +- `name` <[string]|[RegExp]> + +A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + +Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + +## locator-get-by-role-option-pressed +* since: v1.27 +- `pressed` <[boolean]> + +An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. + +Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed). + +## locator-get-by-role-option-selected +* since: v1.27 +- `selected` + +A boolean attribute that is usually set by `aria-selected`. + +Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected). + +## locator-get-by-role-option-list-v1.27 +- %%-locator-get-by-role-option-checked-%% +- %%-locator-get-by-role-option-disabled-%% +- %%-locator-get-by-role-option-expanded-%% +- %%-locator-get-by-role-option-includeHidden-%% +- %%-locator-get-by-role-option-level-%% +- %%-locator-get-by-role-option-name-%% +- %%-locator-get-by-role-option-pressed-%% +- %%-locator-get-by-role-option-selected-%% + ## template-locator-locator The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, similar to [`method: Locator.filter`] method. + +[Learn more about locators](../locators.md). + +## template-locator-root-locator + +The method returns an element locator that can be used to perform actions on this page / frame. +Locator is resolved to the element immediately before performing an action, so a series of actions on the same locator can in fact be performed on different DOM elements. That would happen if the DOM structure between those actions has changed. + +[Learn more about locators](../locators.md). + +## template-locator-get-by-text + +Allows locating elements that contain given text. + +## 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. + +Note that many html elements have an implicitly [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. + diff --git a/packages/html-reporter/src/chip.spec.tsx b/packages/html-reporter/src/chip.spec.tsx index f5b114bec1..0329e49d45 100644 --- a/packages/html-reporter/src/chip.spec.tsx +++ b/packages/html-reporter/src/chip.spec.tsx @@ -23,11 +23,11 @@ test('expand collapse', async ({ mount }) => { const component = await mount( Chip body ); - await expect(component.locator('text=Chip body')).toBeVisible(); - await component.locator('text=Title').click(); - await expect(component.locator('text=Chip body')).not.toBeVisible(); - await component.locator('text=Title').click(); - await expect(component.locator('text=Chip body')).toBeVisible(); + await expect(component.getByText('Chip body')).toBeVisible(); + await component.getByText('Title').click(); + await expect(component.getByText('Chip body')).not.toBeVisible(); + await component.getByText('Title').click(); + await expect(component.getByText('Chip body')).toBeVisible(); await expect(component).toHaveScreenshot(); }); @@ -37,7 +37,7 @@ test('render long title', async ({ mount }) => { Chip body ); await expect(component).toContainText('Extremely long title.'); - await expect(component.locator('text=Extremely long title.')).toHaveAttribute('title', title); + await expect(component.getByText('Extremely long title.')).toHaveAttribute('title', title); await expect(component).toHaveScreenshot(); }); @@ -47,6 +47,6 @@ test('setExpanded is called', async ({ mount }) => { setExpanded={(expanded: boolean) => expandedValues.push(expanded)}> ); - await component.locator('text=Title').click(); + await component.getByText('Title').click(); expect(expandedValues).toEqual([true]); }); diff --git a/packages/html-reporter/src/headerView.spec.tsx b/packages/html-reporter/src/headerView.spec.tsx index 363d3aebea..b37befd22e 100644 --- a/packages/html-reporter/src/headerView.spec.tsx +++ b/packages/html-reporter/src/headerView.spec.tsx @@ -29,11 +29,11 @@ test('should render counters', async ({ mount }) => { ok: false, duration: 100000 }} filterText='' setFilterText={() => {}}>); - await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100'); - await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42'); - await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31'); - await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17'); - await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10'); + await expect(component.get('a', { hasText: 'All' }).get('.counter')).toHaveText('100'); + await expect(component.get('a', { hasText: 'Passed' }).get('.counter')).toHaveText('42'); + await expect(component.get('a', { hasText: 'Failed' }).get('.counter')).toHaveText('31'); + await expect(component.get('a', { hasText: 'Flaky' }).get('.counter')).toHaveText('17'); + await expect(component.get('a', { hasText: 'Skipped' }).get('.counter')).toHaveText('10'); }); test('should toggle filters', async ({ page, mount: mount }) => { @@ -51,14 +51,14 @@ test('should toggle filters', async ({ page, mount: mount }) => { filterText='' setFilterText={(filterText: string) => filters.push(filterText)}> ); - await component.locator('a', { hasText: 'All' }).click(); - await component.locator('a', { hasText: 'Passed' }).click(); + await component.get('a', { hasText: 'All' }).click(); + await component.get('a', { hasText: 'Passed' }).click(); await expect(page).toHaveURL(/#\?q=s:passed/); - await component.locator('a', { hasText: 'Failed' }).click(); + await component.get('a', { hasText: 'Failed' }).click(); await expect(page).toHaveURL(/#\?q=s:failed/); - await component.locator('a', { hasText: 'Flaky' }).click(); + await component.get('a', { hasText: 'Flaky' }).click(); await expect(page).toHaveURL(/#\?q=s:flaky/); - await component.locator('a', { hasText: 'Skipped' }).click(); + await component.get('a', { hasText: 'Skipped' }).click(); await expect(page).toHaveURL(/#\?q=s:skipped/); expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']); }); diff --git a/packages/html-reporter/src/imageDiffView.spec.tsx b/packages/html-reporter/src/imageDiffView.spec.tsx index bfd622d45d..f72b70dc3f 100644 --- a/packages/html-reporter/src/imageDiffView.spec.tsx +++ b/packages/html-reporter/src/imageDiffView.spec.tsx @@ -37,7 +37,7 @@ const imageDiff: ImageDiff = { test('should render links', async ({ mount }) => { const component = await mount(); - await expect(component.locator('a')).toHaveText([ + await expect(component.get('a')).toHaveText([ 'screenshot-actual.png', 'screenshot-expected.png', 'screenshot-diff.png', @@ -46,11 +46,11 @@ test('should render links', async ({ mount }) => { test('should show actual by default', async ({ mount }) => { const component = await mount(); - const sliderElement = component.locator('data-testid=test-result-image-mismatch-grip'); + const sliderElement = component.get('data-testid=test-result-image-mismatch-grip'); await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Actual slider is on the right').toBe('611px'); - const images = component.locator('img'); - const imageCount = await component.locator('img').count(); + const images = component.get('img'); + const imageCount = await component.get('img').count(); for (let i = 0; i < imageCount; ++i) { const image = images.nth(i); const box = await image.boundingBox(); @@ -60,12 +60,12 @@ test('should show actual by default', async ({ mount }) => { test('should switch to expected', async ({ mount }) => { const component = await mount(); - await component.locator('text="Expected"').click(); - const sliderElement = component.locator('data-testid=test-result-image-mismatch-grip'); + await component.getByText('Expected', { exact: true }).click(); + const sliderElement = component.get('data-testid=test-result-image-mismatch-grip'); await expect.poll(() => sliderElement.evaluate(e => e.style.left), 'Expected slider is on the left').toBe('371px'); - const images = component.locator('img'); - const imageCount = await component.locator('img').count(); + const images = component.get('img'); + const imageCount = await component.get('img').count(); for (let i = 0; i < imageCount; ++i) { const image = images.nth(i); const box = await image.boundingBox(); @@ -75,9 +75,9 @@ test('should switch to expected', async ({ mount }) => { test('should switch to diff', async ({ mount }) => { const component = await mount(); - await component.locator('text="Diff"').click(); + await component.getByText('Diff', { exact: true }).click(); - const image = component.locator('img'); + const image = component.get('img'); const box = await image.boundingBox(); expect(box).toEqual({ x: 400, y: 80, width: 200, height: 200 }); }); diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 3b9733ec33..f67a3f2cc5 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -63,13 +63,13 @@ const testCase: TestCase = { test('should render test case', async ({ mount }) => { const component = await mount(); - await expect(component.locator('text=Annotation text').first()).toBeVisible(); - await component.locator('text=Annotations').click(); - await expect(component.locator('text=Annotation text')).not.toBeVisible(); - await expect(component.locator('text=Outer step')).toBeVisible(); - await expect(component.locator('text=Inner step')).not.toBeVisible(); - await component.locator('text=Outer step').click(); - await expect(component.locator('text=Inner step')).toBeVisible(); - await expect(component.locator('text=test.spec.ts:42')).toBeVisible(); - await expect(component.locator('text=My test')).toBeVisible(); + await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); + await component.getByText('Annotations').click(); + await expect(component.getByText('Annotation text')).not.toBeVisible(); + await expect(component.getByText('Outer step')).toBeVisible(); + await expect(component.getByText('Inner step')).not.toBeVisible(); + await component.getByText('Outer step').click(); + await expect(component.getByText('Inner step')).toBeVisible(); + await expect(component.getByText('test.spec.ts:42')).toBeVisible(); + await expect(component.getByText('My test')).toBeVisible(); }); diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 1b254d6f2f..886a7b5b64 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -18,7 +18,8 @@ import { assert } from '../utils'; import type * as channels from '@protocol/channels'; import { ChannelOwner } from './channelOwner'; -import { FrameLocator, Locator, type LocatorOptions } from './locator'; +import { FrameLocator, Locator } from './locator'; +import type { ByRoleOptions, LocatorOptions } from './locator'; import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle'; import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle'; import fs from 'fs'; @@ -298,6 +299,18 @@ export class Frame extends ChannelOwner implements api.Fr return new Locator(this, selector, options); } + get(selector: string, options?: LocatorOptions): Locator { + return this.locator(selector, options); + } + + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByTextSelector(text, options)); + } + + getByRole(role: string, options: ByRoleOptions = {}): Locator { + return this.locator(Locator.getByRoleSelector(role, options)); + } + frameLocator(selector: string): FrameLocator { return new FrameLocator(this, selector); } diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index d50a9a8e9b..afd4143280 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -19,7 +19,7 @@ import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; import type { ParsedStackTrace } from '../utils/stackTrace'; import * as util from 'util'; -import { isRegExp, monotonicTime } from '../utils'; +import { isRegExp, isString, monotonicTime } from '../utils'; import { ElementHandle } from './elementHandle'; import type { Frame } from './frame'; import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; @@ -31,10 +31,50 @@ export type LocatorOptions = { has?: Locator; }; +export type ByRoleOptions = LocatorOptions & { + checked?: boolean; + disabled?: boolean; + expanded?: boolean; + includeHidden?: boolean; + level?: number; + name?: string | RegExp; + pressed?: boolean; + selected?: boolean; +}; + export class Locator implements api.Locator { _frame: Frame; _selector: string; + static getByTextSelector(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; + } + + static getByRoleSelector(role: string, options: ByRoleOptions = {}): string { + const props: string[][] = []; + if (options.checked !== undefined) + props.push(['checked', String(options.checked)]); + if (options.disabled !== undefined) + props.push(['disabled', String(options.disabled)]); + if (options.selected !== undefined) + props.push(['selected', String(options.selected)]); + if (options.expanded !== undefined) + props.push(['expanded', String(options.expanded)]); + if (options.includeHidden !== undefined) + props.push(['include-hidden', String(options.includeHidden)]); + if (options.level !== undefined) + props.push(['level', String(options.level)]); + if (options.name !== undefined) + props.push(['name', isString(options.name) ? escapeWithQuotes(options.name, '"') : String(options.name)]); + if (options.pressed !== undefined) + props.push(['pressed', String(options.pressed)]); + return `role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`; + } + constructor(frame: Frame, selector: string, options?: LocatorOptions) { this._frame = frame; this._selector = selector; @@ -132,6 +172,18 @@ export class Locator implements api.Locator { return new Locator(this._frame, this._selector + ' >> ' + selector, options); } + get(selector: string, options?: LocatorOptions): Locator { + return this.locator(selector, options); + } + + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByTextSelector(text, options)); + } + + getByRole(role: string, options: ByRoleOptions = {}): Locator { + return this.locator(Locator.getByRoleSelector(role, options)); + } + frameLocator(selector: string): FrameLocator { return new FrameLocator(this._frame, this._selector + ' >> ' + selector); } @@ -306,6 +358,18 @@ export class FrameLocator implements api.FrameLocator { return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector, options); } + get(selector: string, options?: LocatorOptions): Locator { + return this.locator(selector, options); + } + + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.locator(Locator.getByTextSelector(text, options)); + } + + getByRole(role: string, options: ByRoleOptions = {}): Locator { + return this.locator(Locator.getByRoleSelector(role, options)); + } + frameLocator(selector: string): FrameLocator { return new FrameLocator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 2319b7a514..dc13cd37b6 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -45,7 +45,7 @@ import { Frame, verifyLoadState } from './frame'; import { HarRouter } from './harRouter'; import { Keyboard, Mouse, Touchscreen } from './input'; import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle'; -import type { FrameLocator, Locator, LocatorOptions } from './locator'; +import type { ByRoleOptions, FrameLocator, Locator, LocatorOptions } from './locator'; import type { RouteHandlerCallback } from './network'; import { Response, Route, RouteHandler, validateHeaders, WebSocket } from './network'; import type { Request } from './network'; @@ -564,6 +564,18 @@ export class Page extends ChannelOwner implements api.Page return this.mainFrame().locator(selector, options); } + get(selector: string, options?: LocatorOptions): Locator { + return this.mainFrame().locator(selector, options); + } + + getByText(text: string | RegExp, options?: { exact?: boolean }): Locator { + return this.mainFrame().getByText(text, options); + } + + getByRole(role: string, options: ByRoleOptions = {}): Locator { + return this.mainFrame().getByRole(role, options); + } + frameLocator(selector: string): FrameLocator { return this.mainFrame().frameLocator(selector); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index ec7130f690..81a831e48b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2434,6 +2434,32 @@ export interface Page { */ frames(): Array; + /** + * The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to + * the element immediately before performing an action, so a series of actions on the same locator can in fact be performed + * on different DOM elements. That would happen if the DOM structure between those actions has changed. + * + * [Learn more about locators](https://playwright.dev/docs/locators). + * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options + */ + get(selector: string, options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; + /** * Returns element attribute value. * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. @@ -2456,6 +2482,90 @@ export interface Page { timeout?: number; }): Promise; + /** + * 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. + * + * Note that many html elements have an implicitly + * [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 options + */ + getByRole(role: string, options?: { + /** + * An attribute that is usually set by `aria-checked` or native `` controls. Available values for + * checked are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked). + */ + checked?: boolean; + + /** + * A boolean attribute that is usually set by `aria-disabled` or `disabled`. + * + * > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about + * [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled). + */ + disabled?: boolean; + + /** + * A boolean attribute that is usually set by `aria-expanded`. + * + * Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). + */ + expanded?: boolean; + + /** + * A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as + * [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector. + * + * Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden). + */ + includeHidden?: boolean; + + /** + * A number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for + * `

-

` elements. + * + * Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level). + */ + level?: number; + + /** + * A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + * + * Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + */ + name?: string|RegExp; + + /** + * An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed). + */ + pressed?: boolean; + + /** + * A boolean attribute that is usually set by `aria-selected`. + * + * Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected). + */ + selected?: boolean; + }): Locator; + + /** + * Allows locating elements that contain given text. + * @param text + * @param options + */ + getByText(text: string|RegExp, options?: { + exact?: boolean; + }): Locator; + /** * Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the * last redirect. If can not go back, returns `null`. @@ -2826,14 +2936,11 @@ export interface Page { keyboard: Keyboard; /** - * The method returns an element locator that can be used to perform actions on the page. Locator is resolved to the - * element immediately before performing an action, so a series of actions on the same locator can in fact be performed on - * different DOM elements. That would happen if the DOM structure between those actions has changed. + * The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to + * the element immediately before performing an action, so a series of actions on the same locator can in fact be performed + * on different DOM elements. That would happen if the DOM structure between those actions has changed. * * [Learn more about locators](https://playwright.dev/docs/locators). - * - * Shortcut for main frame's - * [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator). * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ @@ -5357,6 +5464,32 @@ export interface Frame { */ frameLocator(selector: string): FrameLocator; + /** + * The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to + * the element immediately before performing an action, so a series of actions on the same locator can in fact be performed + * on different DOM elements. That would happen if the DOM structure between those actions has changed. + * + * [Learn more about locators](https://playwright.dev/docs/locators). + * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options + */ + get(selector: string, options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; + /** * Returns element attribute value. * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. @@ -5379,6 +5512,90 @@ export interface Frame { timeout?: number; }): Promise; + /** + * 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. + * + * Note that many html elements have an implicitly + * [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 options + */ + getByRole(role: string, options?: { + /** + * An attribute that is usually set by `aria-checked` or native `` controls. Available values for + * checked are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked). + */ + checked?: boolean; + + /** + * A boolean attribute that is usually set by `aria-disabled` or `disabled`. + * + * > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about + * [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled). + */ + disabled?: boolean; + + /** + * A boolean attribute that is usually set by `aria-expanded`. + * + * Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). + */ + expanded?: boolean; + + /** + * A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as + * [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector. + * + * Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden). + */ + includeHidden?: boolean; + + /** + * A number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for + * `

-

` elements. + * + * Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level). + */ + level?: number; + + /** + * A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + * + * Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + */ + name?: string|RegExp; + + /** + * An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed). + */ + pressed?: boolean; + + /** + * A boolean attribute that is usually set by `aria-selected`. + * + * Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected). + */ + selected?: boolean; + }): Locator; + + /** + * Allows locating elements that contain given text. + * @param text + * @param options + */ + getByText(text: string|RegExp, options?: { + exact?: boolean; + }): Locator; + /** * Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the * last redirect. @@ -5686,9 +5903,11 @@ export interface Frame { }): Promise; /** - * The method returns an element locator that can be used to perform actions in the frame. Locator is resolved to the - * element immediately before performing an action, so a series of actions on the same locator can in fact be performed on - * different DOM elements. That would happen if the DOM structure between those actions has changed. + * The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to + * the element immediately before performing an action, so a series of actions on the same locator can in fact be performed + * on different DOM elements. That would happen if the DOM structure between those actions has changed. + * + * [Learn more about locators](https://playwright.dev/docs/locators). * * [Learn more about locators](https://playwright.dev/docs/locators). * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. @@ -9630,6 +9849,31 @@ export interface Locator { */ frameLocator(selector: string): FrameLocator; + /** + * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, + * similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) method. + * + * [Learn more about locators](https://playwright.dev/docs/locators). + * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options + */ + get(selector: string, options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; + /** * Returns element attribute value. * @param name Attribute name to get the value for. @@ -9645,6 +9889,90 @@ export interface Locator { timeout?: number; }): Promise; + /** + * 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. + * + * Note that many html elements have an implicitly + * [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 options + */ + getByRole(role: string, options?: { + /** + * An attribute that is usually set by `aria-checked` or native `` controls. Available values for + * checked are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked). + */ + checked?: boolean; + + /** + * A boolean attribute that is usually set by `aria-disabled` or `disabled`. + * + * > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about + * [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled). + */ + disabled?: boolean; + + /** + * A boolean attribute that is usually set by `aria-expanded`. + * + * Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). + */ + expanded?: boolean; + + /** + * A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as + * [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector. + * + * Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden). + */ + includeHidden?: boolean; + + /** + * A number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for + * `

-

` elements. + * + * Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level). + */ + level?: number; + + /** + * A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + * + * Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + */ + name?: string|RegExp; + + /** + * An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed). + */ + pressed?: boolean; + + /** + * A boolean attribute that is usually set by `aria-selected`. + * + * Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected). + */ + selected?: boolean; + }): Locator; + + /** + * Allows locating elements that contain given text. + * @param text + * @param options + */ + getByText(text: string|RegExp, options?: { + exact?: boolean; + }): Locator; + /** * Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses * [locator.highlight()](https://playwright.dev/docs/api/class-locator#locator-highlight). @@ -9839,6 +10167,8 @@ export interface Locator { /** * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, * similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) method. + * + * [Learn more about locators](https://playwright.dev/docs/locators). * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ @@ -14732,6 +15062,115 @@ export interface FrameLocator { */ frameLocator(selector: string): FrameLocator; + /** + * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, + * similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) method. + * + * [Learn more about locators](https://playwright.dev/docs/locators). + * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options + */ + get(selector: string, options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): 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 + * [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. + * + * Note that many html elements have an implicitly + * [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 options + */ + getByRole(role: string, options?: { + /** + * An attribute that is usually set by `aria-checked` or native `` controls. Available values for + * checked are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked). + */ + checked?: boolean; + + /** + * A boolean attribute that is usually set by `aria-disabled` or `disabled`. + * + * > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about + * [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled). + */ + disabled?: boolean; + + /** + * A boolean attribute that is usually set by `aria-expanded`. + * + * Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). + */ + expanded?: boolean; + + /** + * A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as + * [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector. + * + * Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden). + */ + includeHidden?: boolean; + + /** + * A number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for + * `

-

` elements. + * + * Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level). + */ + level?: number; + + /** + * A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + * + * Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). + */ + name?: string|RegExp; + + /** + * An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. + * + * Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed). + */ + pressed?: boolean; + + /** + * A boolean attribute that is usually set by `aria-selected`. + * + * Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected). + */ + selected?: boolean; + }): Locator; + + /** + * Allows locating elements that contain given text. + * @param text + * @param options + */ + getByText(text: string|RegExp, options?: { + exact?: boolean; + }): Locator; + /** * Returns locator to the last matching frame. */ @@ -14740,6 +15179,8 @@ export interface FrameLocator { /** * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, * similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) method. + * + * [Learn more about locators](https://playwright.dev/docs/locators). * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 888e87c05a..93946f75c0 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -580,15 +580,15 @@ test('should include metainfo', async ({ showTraceViewer, browserName }) => { const traceViewer = await showTraceViewer([traceFile]); await traceViewer.page.locator('text=Metadata').click(); const callLine = traceViewer.page.locator('.call-line'); - await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/); - await expect(callLine.locator('text=duration')).toHaveText(/duration: [\dms]+/); - await expect(callLine.locator('text=engine')).toHaveText(/engine: [\w]+/); - await expect(callLine.locator('text=platform')).toHaveText(/platform: [\w]+/); - await expect(callLine.locator('text=width')).toHaveText(/width: [\d]+/); - await expect(callLine.locator('text=height')).toHaveText(/height: [\d]+/); - await expect(callLine.locator('text=pages')).toHaveText(/pages: 1/); - await expect(callLine.locator('text=actions')).toHaveText(/actions: [\d]+/); - await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/); + await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/); + await expect(callLine.getByText('duration')).toHaveText(/duration: [\dms]+/); + await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/); + await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/); + await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/); + await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/); + await expect(callLine.getByText('pages')).toHaveText(/pages: 1/); + await expect(callLine.getByText('actions')).toHaveText(/actions: [\d]+/); + await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/); }); test('should open two trace files', async ({ context, page, request, server, showTraceViewer }, testInfo) => { @@ -631,16 +631,16 @@ test('should open two trace files', async ({ context, page, request, server, sho await traceViewer.page.locator('text=Metadata').click(); const callLine = traceViewer.page.locator('.call-line'); // Should get metadata from the context trace - await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/); + await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/); // duration in the metatadata section - await expect(callLine.locator('text=duration').first()).toHaveText(/duration: [\dms]+/); - await expect(callLine.locator('text=engine')).toHaveText(/engine: [\w]+/); - await expect(callLine.locator('text=platform')).toHaveText(/platform: [\w]+/); - await expect(callLine.locator('text=width')).toHaveText(/width: [\d]+/); - await expect(callLine.locator('text=height')).toHaveText(/height: [\d]+/); - await expect(callLine.locator('text=pages')).toHaveText(/pages: 1/); - await expect(callLine.locator('text=actions')).toHaveText(/actions: 6/); - await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/); + await expect(callLine.getByText('duration').first()).toHaveText(/duration: [\dms]+/); + await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/); + await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/); + await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/); + await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/); + await expect(callLine.getByText('pages')).toHaveText(/pages: 1/); + await expect(callLine.getByText('actions')).toHaveText(/actions: 6/); + await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/); }); test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, browserName }) => { @@ -661,8 +661,8 @@ test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, b await traceViewer.selectAction('route.fulfill'); await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click(); const callLine = traceViewer.page.locator('.call-line'); - await expect(callLine.locator('text=status')).toContainText('200'); - await expect(callLine.locator('text=requestUrl')).toContainText('http://test.com'); + await expect(callLine.getByText('status')).toContainText('200'); + await expect(callLine.getByText('requestUrl')).toContainText('http://test.com'); }); test('should include requestUrl in route.continue', async ({ page, runAndTrace, server }) => { @@ -677,8 +677,8 @@ test('should include requestUrl in route.continue', async ({ page, runAndTrace, await traceViewer.selectAction('route.continue'); await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click(); const callLine = traceViewer.page.locator('.call-line'); - await expect(callLine.locator('text=requestUrl')).toContainText('http://test.com'); - await expect(callLine.locator('text=/^url: .*/')).toContainText(server.EMPTY_PAGE); + await expect(callLine.getByText('requestUrl')).toContainText('http://test.com'); + await expect(callLine.getByText(/^url: .*/)).toContainText(server.EMPTY_PAGE); }); test('should include requestUrl in route.abort', async ({ page, runAndTrace, server }) => { @@ -693,7 +693,7 @@ test('should include requestUrl in route.abort', async ({ page, runAndTrace, ser await traceViewer.selectAction('route.abort'); await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click(); const callLine = traceViewer.page.locator('.call-line'); - await expect(callLine.locator('text=requestUrl')).toContainText('http://test.com'); + await expect(callLine.getByText('requestUrl')).toContainText('http://test.com'); }); test('should serve overridden request', async ({ page, runAndTrace, server }) => { diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts index 83b8289529..e080cbbd72 100644 --- a/tests/page/locator-frame.spec.ts +++ b/tests/page/locator-frame.spec.ts @@ -68,7 +68,7 @@ async function routeAmbiguous(page: Page) { it('should work for iframe @smoke', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.frameLocator('iframe').locator('button'); + const button = page.frameLocator('iframe').get('button'); await button.waitFor(); expect(await button.innerText()).toBe('Hello iframe'); await expect(button).toHaveText('Hello iframe'); @@ -78,7 +78,7 @@ it('should work for iframe @smoke', async ({ page, server }) => { it('should work for nested iframe', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.frameLocator('iframe').frameLocator('iframe').locator('button'); + const button = page.frameLocator('iframe').frameLocator('iframe').get('button'); await button.waitFor(); expect(await button.innerText()).toBe('Hello nested iframe'); await expect(button).toHaveText('Hello nested iframe'); @@ -88,15 +88,15 @@ it('should work for nested iframe', async ({ page, server }) => { it('should work for $ and $$', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const locator = page.frameLocator('iframe').locator('button'); + const locator = page.frameLocator('iframe').get('button'); await expect(locator).toHaveText('Hello iframe'); - const spans = page.frameLocator('iframe').locator('span'); + const spans = page.frameLocator('iframe').get('span'); await expect(spans).toHaveCount(2); }); it('should wait for frame', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); - const error = await page.frameLocator('iframe').locator('span').click({ timeout: 1000 }).catch(e => e); + const error = await page.frameLocator('iframe').get('span').click({ timeout: 1000 }).catch(e => e); expect(error.message).toContain('waiting for frame "iframe"'); }); @@ -237,3 +237,12 @@ it('locator.frameLocator should not throw on first/last/nth', async ({ page, ser const button3 = page.locator('body').frameLocator('iframe').last().locator('button'); await expect(button3).toHaveText('Hello from iframe-3.html'); }); + +it('role and text coverage', async ({ page, server }) => { + await routeIframe(page); + await page.goto(server.EMPTY_PAGE); + const button1 = page.frameLocator('iframe').getByRole('button'); + const button2 = page.frameLocator('iframe').getByText('Hello'); + await expect(button1).toHaveText('Hello iframe'); + await expect(button2).toHaveText('Hello iframe'); +}); diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 29742c1531..6d729d4c90 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -172,3 +172,11 @@ it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ expect(error.message).toContain(`Inner "${option}" locator must belong to the same frame.`); } }); + +it('alias methods coverage', async ({ page }) => { + await page.setContent(`
`); + await expect(page.get('button')).toHaveCount(1); + await expect(page.get('div').get('button')).toHaveCount(1); + await expect(page.get('div').getByRole('button')).toHaveCount(1); + await expect(page.mainFrame().get('button')).toHaveCount(1); +}); diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts index 4b024d466d..bd79fa2d61 100644 --- a/tests/page/selectors-role.spec.ts +++ b/tests/page/selectors-role.spec.ts @@ -25,25 +25,27 @@ test('should detect roles', async ({ page }) => {
Hello
I am a dialog
`); - expect(await page.$$eval(`role=button`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=listbox`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=listbox`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=combobox`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=combobox`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=heading`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=heading`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `

Heading

`, ]); - expect(await page.$$eval(`role=group`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=group`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
Hello
`, ]); - expect(await page.$$eval(`role=dialog`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=dialog`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
I am a dialog
`, ]); - expect(await page.$$eval(`role=menuitem`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=menuitem`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ]); + expect(await page.getByRole('menuitem').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ]); }); @@ -58,15 +60,25 @@ test('should support selected', async ({ page }) => {
Hello
`); - expect(await page.$$eval(`role=option[selected]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=option[selected]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hi
`, ]); - expect(await page.$$eval(`role=option[selected=true]`, els => els.map(e => e.outerHTML))).toEqual([ + + expect(await page.get(`role=option[selected=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hi
`, ]); - expect(await page.$$eval(`role=option[selected=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('option', { selected: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + `
Hi
`, + ]); + + expect(await page.get(`role=option[selected=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + `
Hello
`, + ]); + expect(await page.getByRole('option', { selected: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hello
`, ]); @@ -82,23 +94,35 @@ test('should support checked', async ({ page }) => {
Unknown
`); await page.$eval('[indeterminate]', input => (input as HTMLInputElement).indeterminate = true); - expect(await page.$$eval(`role=checkbox[checked]`, els => els.map(e => e.outerHTML))).toEqual([ + + expect(await page.get(`role=checkbox[checked]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hi
`, ]); - expect(await page.$$eval(`role=checkbox[checked=true]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=checkbox[checked=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hi
`, ]); - expect(await page.$$eval(`role=checkbox[checked=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('checkbox', { checked: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + `
Hi
`, + ]); + + expect(await page.get(`role=checkbox[checked=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, `
Hello
`, `
Unknown
`, ]); - expect(await page.$$eval(`role=checkbox[checked="mixed"]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('checkbox', { checked: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + `
Hello
`, + `
Unknown
`, + ]); + + expect(await page.get(`role=checkbox[checked="mixed"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=checkbox`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=checkbox`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, @@ -115,20 +139,27 @@ test('should support pressed', async ({ page }) => { `); - expect(await page.$$eval(`role=button[pressed]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[pressed]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=button[pressed=true]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[pressed=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=button[pressed=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { pressed: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + expect(await page.get(`role=button[pressed=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ]); - expect(await page.$$eval(`role=button[pressed="mixed"]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { pressed: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ]); + expect(await page.get(`role=button[pressed="mixed"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=button`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, @@ -142,13 +173,20 @@ test('should support expanded', async ({ page }) => { `); - expect(await page.$$eval(`role=button[expanded]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[expanded]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=button[expanded=true]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[expanded=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); - expect(await page.$$eval(`role=button[expanded=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { expanded: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + expect(await page.get(`role=button[expanded=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ]); + expect(await page.getByRole('button', { expanded: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ]); @@ -164,17 +202,26 @@ test('should support disabled', async ({ page }) => { `); - expect(await page.$$eval(`role=button[disabled]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[disabled]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, ]); - expect(await page.$$eval(`role=button[disabled=true]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[disabled=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, ]); - expect(await page.$$eval(`role=button[disabled=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { disabled: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ``, + ]); + expect(await page.get(`role=button[disabled=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ]); + expect(await page.getByRole('button', { disabled: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ]); @@ -186,13 +233,19 @@ test('should support level', async ({ page }) => {

Hi

Bye
`); - expect(await page.$$eval(`role=heading[level=1]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=heading[level=1]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `

Hello

`, ]); - expect(await page.$$eval(`role=heading[level=3]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('heading', { level: 1 }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `

Hello

`, + ]); + expect(await page.get(`role=heading[level=3]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `

Hi

`, ]); - expect(await page.$$eval(`role=heading[level=5]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('heading', { level: 3 }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `

Hi

`, + ]); + expect(await page.get(`role=heading[level=5]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
Bye
`, ]); }); @@ -224,13 +277,13 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => { addButton(document.getElementById('host2'), 'Shadow2'); `); - expect(await page.$$eval(`role=button`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, ``, ]); - expect(await page.$$eval(`role=button[include-hidden]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, @@ -242,7 +295,7 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => { ``, ``, ]); - expect(await page.$$eval(`role=button[include-hidden=true]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[include-hidden=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, @@ -254,7 +307,7 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => { ``, ``, ]); - expect(await page.$$eval(`role=button[include-hidden=false]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[include-hidden=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ``, ``, @@ -268,29 +321,53 @@ test('should support name', async ({ page }) => {
+ `); - expect(await page.$$eval(`role=button[name="Hello"]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[name="Hello"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, ]); - expect(await page.$$eval(`role=button[name*="all"]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { name: 'Hello' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
`, + ]); + + expect(await page.get(`role=button[name*="all"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, ]); - expect(await page.$$eval(`role=button[name=/^H[ae]llo$/]`, els => els.map(e => e.outerHTML))).toEqual([ + + expect(await page.get(`role=button[name=/^H[ae]llo$/]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, `
`, ]); - expect(await page.$$eval(`role=button[name=/h.*o/i]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { name: /^H[ae]llo$/ }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, `
`, ]); - expect(await page.$$eval(`role=button[name="Hello"][include-hidden]`, els => els.map(e => e.outerHTML))).toEqual([ + + expect(await page.get(`role=button[name=/h.*o/i]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
`, + `
`, + ]); + expect(await page.getByRole('button', { name: /h.*o/i }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
`, + `
`, + ]); + + expect(await page.get(`role=button[name="Hello"][include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, ``, ]); - expect(await page.$$eval(`role=button[name=Hello]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.getByRole('button', { name: 'Hello', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
`, + ``, + ]); + + expect(await page.get(`role=button[name=Hello]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`, ]); - expect(await page.$$eval(`role=button[name=123][include-hidden]`, els => els.map(e => e.outerHTML))).toEqual([ + expect(await page.get(`role=button[name=123][include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + expect(await page.getByRole('button', { name: '123', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ ``, ]); }); diff --git a/tests/page/selectors-text.spec.ts b/tests/page/selectors-text.spec.ts index 002e24e2ca..36fcb97342 100644 --- a/tests/page/selectors-text.spec.ts +++ b/tests/page/selectors-text.spec.ts @@ -24,9 +24,11 @@ it('should work @smoke', async ({ page }) => { expect(await page.$eval(`text=/^[ay]+$/`, e => e.outerHTML)).toBe('
ya
'); expect(await page.$eval(`text=/Ya/i`, e => e.outerHTML)).toBe('
ya
'); expect(await page.$eval(`text=ye`, e => e.outerHTML)).toBe('
\nye
'); + expect(await page.getByText('ye').evaluate(e => e.outerHTML)).toBe('
\nye
'); await page.setContent(`
ye
ye
`); expect(await page.$eval(`text="ye"`, e => e.outerHTML)).toBe('
ye
'); + expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toBe('
ye
'); await page.setContent(`
yo
"ya
hello world!
`); expect(await page.$eval(`text="\\"ya"`, e => e.outerHTML)).toBe('
"ya
');