From 827fb80465c2da618a4a036ae7a90d87b436df6f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 22 Jul 2021 21:37:20 -0700 Subject: [PATCH] feat(locator): implement element locators (#7808) --- docs/src/api/class-elementhandle.md | 4 +- docs/src/api/class-frame.md | 10 + docs/src/api/class-locator.md | 849 ++++++++++++ docs/src/api/class-page.md | 13 +- docs/src/api/javascript.md | 3 + docs/src/api/params.md | 5 + src/client/api.ts | 1 + src/client/frame.ts | 5 + src/client/locator.ts | 198 +++ src/client/page.ts | 5 + tests/page/locator-click.spec.ts | 70 + tests/page/locator-convenience.spec.ts | 198 +++ tests/page/locator-evaluate.spec.ts | 56 + tests/page/locator-misc-1.spec.ts | 106 ++ tests/page/locator-misc-2.spec.ts | 105 ++ ...reenshot-element-bounding-box-chromium.png | Bin 0 -> 474 bytes tests/page/locator-query-selector.spec.ts | 62 + types/structs.d.ts | 2 +- types/types.d.ts | 1139 +++++++++++++++++ utils/generate_types/overrides.d.ts | 11 + 20 files changed, 2838 insertions(+), 4 deletions(-) create mode 100644 docs/src/api/class-locator.md create mode 100644 src/client/locator.ts create mode 100644 tests/page/locator-click.spec.ts create mode 100644 tests/page/locator-convenience.spec.ts create mode 100644 tests/page/locator-evaluate.spec.ts create mode 100644 tests/page/locator-misc-1.spec.ts create mode 100644 tests/page/locator-misc-2.spec.ts create mode 100644 tests/page/locator-misc-2.spec.ts-snapshots/screenshot-element-bounding-box-chromium.png create mode 100644 tests/page/locator-query-selector.spec.ts diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index fff60d9d23..25a07f816f 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -304,8 +304,8 @@ element_handle.dispatch_event("#source", "dragstart", {"dataTransfer": data_tran ``` ```csharp -var handle = await page.EvaluateHandleAsync("() => new DataTransfer()"); -await handle.AsElement().DispatchEventAsync("dragstart", new Dictionary +var dataTransfer = await page.EvaluateHandleAsync("() => new DataTransfer()"); +await elementHandle.DispatchEventAsync("dragstart", new Dictionary { { "dataTransfer", dataTransfer } }); diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 44f46bcb2a..14488fcb8e 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -935,6 +935,16 @@ Returns whether the element is [visible](./actionability.md#visible). [`option: ### option: Frame.isVisible.timeout = %%-input-timeout-%% +## method: Frame.locator +- 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. + +Note that locator always implies visibility, so it will always be locating visible elements. + +### param: Frame.locator.selector = %%-find-selector-%% + ## method: Frame.name - returns: <[string]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md new file mode 100644 index 0000000000..4187e4c3e6 --- /dev/null +++ b/docs/src/api/class-locator.md @@ -0,0 +1,849 @@ +# class: Locator + +Locator represents a view to the element(s) on the page. It captures the logic sufficient to retrieve the element at any given moment. Locator can be created with the [`method: Page.locator`] method. + +The difference between the Locator and [ElementHandle] is that the latter points to a particular element, while Locator only captures the logic of how to retrieve an element at any given moment. + +In the example below, handle points to a particular DOM element on page. If that element changes text or is used by React to render an entirely different component, handle is still pointing to that very DOM element. + +```js +const handle = await page.$('text=Submit'); +// ... +await handle.hover(); +await handle.click(); +``` + +```java +ElementHandle handle = page.querySelector("text=Submit"); +handle.hover(); +handle.click(); +``` + +```python async +handle = await page.query_selector("text=Submit") +await handle.hover() +await handle.click() +``` + +```python sync +handle = page.query_selector("text=Submit") +handle.hover() +handle.click() +``` + +```csharp +var handle = await page.QuerySelectorAsync("text=Submit"); +await handle.HoverAsync(); +await handle.ClickAsync(); +``` + +With the locator, every time the `element` is used, corresponding DOM element is located in the page using given selector. So in the snippet below, underlying DOM element is going to be located twice, using the given selector. + +```js +const element = page.locator('text=Submit'); +// ... +await element.hover(); +await element.click(); +``` + +```java +Locator element = page.locator("text=Submit"); +element.hover(); +element.click(); +``` + +```python async +element = page.locator("text=Submit") +await element.hover() +await element.click() +``` + +```python sync +element = page.locator("text=Submit") +element.hover() +element.click() +``` + +```csharp +var element = page.Finder("text=Submit"); +await element.HoverAsync(); +await element.ClickAsync(); +``` + +## async method: Locator.all +- returns: <[Array]<[ElementHandle]>> + +Resolves given locator to all matching DOM elements. + +## async method: Locator.boundingBox +- returns: <[null]|[Object]> + - `x` <[float]> the x coordinate of the element in pixels. + - `y` <[float]> the y coordinate of the element in pixels. + - `width` <[float]> the width of the element in pixels. + - `height` <[float]> the height of the element in pixels. + +This method returns the bounding box of the element, or `null` if the element is not visible. The bounding box is +calculated relative to the main frame viewport - which is usually the same as the browser window. + +Scrolling affects the returned bonding box, similarly to +[Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). That +means `x` and/or `y` may be negative. + +Elements from child frames return the bounding box relative to the main frame, unlike the +[Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). + +Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the following +snippet should click the center of the element. + +```js +const box = await element.boundingBox(); +await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); +``` + +```java +BoundingBox box = element.boundingBox(); +page.mouse().click(box.x + box.width / 2, box.y + box.height / 2); +``` + +```python async +box = await element.bounding_box() +await page.mouse.click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) +``` + +```python sync +box = element.bounding_box() +page.mouse.click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) +``` + +```csharp +var box = await element.BoundingBoxAsync(); +await page.Mouse.ClickAsync(box.X + box.Width / 2, box.Y + box.Height / 2); +``` + +### option: Locator.boundingBox.timeout = %%-input-timeout-%% + +## async method: Locator.check + +This method checks the element by performing the following steps: +1. Ensure that element is a checkbox or a radio input. If not, this method throws. If the element is already + checked, this method returns immediately. +1. Wait for [actionability](./actionability.md) checks on the element, unless [`option: force`] option is set. +1. Scroll the element into view if needed. +1. Use [`property: Page.mouse`] to click in the center of the element. +1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. +1. Ensure that the element is now checked. If not, this method throws. + +If the element is detached from the DOM at any moment during the action, this method throws. + +When all steps combined have not finished during the specified [`option: timeout`], this method throws a +[TimeoutError]. Passing zero timeout disables this. + +### option: Locator.check.position = %%-input-position-%% +### option: Locator.check.force = %%-input-force-%% +### option: Locator.check.noWaitAfter = %%-input-no-wait-after-%% +### option: Locator.check.timeout = %%-input-timeout-%% +### option: Locator.check.trial = %%-input-trial-%% + +## async method: Locator.click + +This method clicks the element by performing the following steps: +1. Wait for [actionability](./actionability.md) checks on the element, unless [`option: force`] option is set. +1. Scroll the element into view if needed. +1. Use [`property: Page.mouse`] to click in the center of the element, or the specified [`option: position`]. +1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. + +If the element is detached from the DOM at any moment during the action, this method throws. + +When all steps combined have not finished during the specified [`option: timeout`], this method throws a +[TimeoutError]. Passing zero timeout disables this. + +### option: Locator.click.button = %%-input-button-%% +### option: Locator.click.clickCount = %%-input-click-count-%% +### option: Locator.click.delay = %%-input-down-up-delay-%% +### option: Locator.click.position = %%-input-position-%% +### option: Locator.click.modifiers = %%-input-modifiers-%% +### option: Locator.click.force = %%-input-force-%% +### option: Locator.click.noWaitAfter = %%-input-no-wait-after-%% +### option: Locator.click.timeout = %%-input-timeout-%% +### option: Locator.click.trial = %%-input-trial-%% + +## async method: Locator.dblclick +* langs: + - alias-csharp: DblClickAsync + +This method double clicks the element by performing the following steps: +1. Wait for [actionability](./actionability.md) checks on the element, unless [`option: force`] option is set. +1. Scroll the element into view if needed. +1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`]. +1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that + if the first click of the `dblclick()` triggers a navigation event, this method will throw. + +If the element is detached from the DOM at any moment during the action, this method throws. + +When all steps combined have not finished during the specified [`option: timeout`], this method throws a +[TimeoutError]. Passing zero timeout disables this. + +:::note +`element.dblclick()` dispatches two `click` events and a single `dblclick` event. +::: + +### option: Locator.dblclick.button = %%-input-button-%% +### option: Locator.dblclick.delay = %%-input-down-up-delay-%% +### option: Locator.dblclick.position = %%-input-position-%% +### option: Locator.dblclick.modifiers = %%-input-modifiers-%% +### option: Locator.dblclick.force = %%-input-force-%% +### option: Locator.dblclick.noWaitAfter = %%-input-no-wait-after-%% +### option: Locator.dblclick.timeout = %%-input-timeout-%% +### option: Locator.dblclick.trial = %%-input-trial-%% + +## async method: Locator.dispatchEvent + +The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, `click` +is dispatched. This is equivalent to calling +[element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). + +```js +await element.dispatchEvent('click'); +``` + +```java +element.dispatchEvent("click"); +``` + +```python async +await element.dispatch_event("click") +``` + +```python sync +element.dispatch_event("click") +``` + +```csharp +await element.DispatchEventAsync("click"); +``` + +Under the hood, it creates an instance of an event based on the given [`param: type`], initializes it with +[`param: eventInit`] properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by +default. + +Since [`param: eventInit`] is event-specific, please refer to the events documentation for the lists of initial +properties: +* [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) +* [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) +* [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) +* [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) +* [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) +* [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) +* [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + +You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: + +```js +// Note you can only create DataTransfer in Chromium and Firefox +const dataTransfer = await page.evaluateHandle(() => new DataTransfer()); +await element.dispatchEvent('dragstart', { dataTransfer }); +``` + +```java +// Note you can only create DataTransfer in Chromium and Firefox +JSHandle dataTransfer = page.evaluateHandle("() => new DataTransfer()"); +Map arg = new HashMap<>(); +arg.put("dataTransfer", dataTransfer); +element.dispatchEvent("dragstart", arg); +``` + +```python async +# note you can only create data_transfer in chromium and firefox +data_transfer = await page.evaluate_handle("new DataTransfer()") +await element.dispatch_event("#source", "dragstart", {"dataTransfer": data_transfer}) +``` + +```python sync +# note you can only create data_transfer in chromium and firefox +data_transfer = page.evaluate_handle("new DataTransfer()") +element.dispatch_event("#source", "dragstart", {"dataTransfer": data_transfer}) +``` + +```csharp +var dataTransfer = await page.EvaluateHandleAsync("() => new DataTransfer()"); +await element.DispatchEventAsync("dragstart", new Dictionary +{ + { "dataTransfer", dataTransfer } +}); +``` + +### param: Locator.dispatchEvent.type +- `type` <[string]> + +DOM event type: `"click"`, `"dragstart"`, etc. + +### param: Locator.dispatchEvent.eventInit +- `eventInit` <[EvaluationArgument]> + +Optional event-specific initialization properties. + +### option: Locator.dispatchEvent.timeout = %%-input-timeout-%% + +## async method: Locator.evaluate +- returns: <[Serializable]> + +Returns the return value of [`param: expression`]. + +This method passes this handle as the first argument to [`param: expression`]. + +If [`param: expression`] returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return +its value. + +Examples: + +```js +const tweets = await page.locator('.tweet .retweets'); +expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets'); +``` + +```java +Locator tweets = page.locator(".tweet .retweets"); +assertEquals("10 retweets", tweets.evaluate("node => node.innerText")); +``` + +```python async +tweets = await page.locator(".tweet .retweets") +assert await tweets.evaluate("node => node.innerText") == "10 retweets" +``` + +```python sync +tweets = page.locator(".tweet .retweets") +assert tweets.evaluate("node => node.innerText") == "10 retweets" +``` + +```csharp +var tweets = page.Finder(".tweet .retweets"); +Assert.Equals("10 retweets", await tweets.EvaluateAsync("node => node.innerText")); +``` + +### param: Locator.evaluate.expression = %%-evaluate-expression-%% + +### param: Locator.evaluate.arg +- `arg` <[EvaluationArgument]> + +Optional argument to pass to [`param: expression`]. + +### option: Locator.evaluate.timeout = %%-input-timeout-%% + +## async method: Locator.evaluateAll +- returns: <[Serializable]> + +The method finds all elements matching the specified locator and passes an array of matched elements as +a first argument to [`param: expression`]. Returns the result of [`param: expression`] invocation. + +If [`param: expression`] returns a [Promise], then [`Locator.evaluateAll`] would wait for the promise +to resolve and return its value. + +Examples: + +```js +const elements = page.locator('div'); +const divCounts = await elements.evaluateAll((divs, min) => divs.length >= min, 10); +``` + +```java +Locator elements = page.locator("div"); +boolean divCounts = (boolean) elements.evaluateAll("(divs, min) => divs.length >= min", 10); +``` + +```python async +elements = page.locator("div") +div_counts = await elements("(divs, min) => divs.length >= min", 10) +``` + +```python sync +elements = page.locator("div") +div_counts = elements("(divs, min) => divs.length >= min", 10) +``` + +```csharp +var elements = page.Locator("div"); +var divsCount = await elements.EvaluateAll("(divs, min) => divs.length >= min", 10); +``` + +### param: Locator.evaluateAll.expression = %%-evaluate-expression-%% + +### param: Locator.evaluateAll.arg +- `arg` <[EvaluationArgument]> + +Optional argument to pass to [`param: expression`]. + + +## async method: Locator.evaluateHandle +- returns: <[JSHandle]> + +Returns the return value of [`param: expression`] as a [JSHandle]. + +This method passes this handle as the first argument to [`param: expression`]. + +The only difference between [`method: Locator.evaluate`] and [`method: Locator.evaluateHandle`] is that [`method: Locator.evaluateHandle`] returns [JSHandle]. + +If the function passed to the [`method: Locator.evaluateHandle`] returns a [Promise], then [`method: Locator.evaluateHandle`] would wait +for the promise to resolve and return its value. + +See [`method: Page.evaluateHandle`] for more details. + +### param: Locator.evaluateHandle.expression = %%-evaluate-expression-%% + +### param: Locator.evaluateHandle.arg +- `arg` <[EvaluationArgument]> + +Optional argument to pass to [`param: expression`]. + +### option: Locator.evaluateHandle.timeout = %%-input-timeout-%% + +## async method: Locator.fill + +This method waits for [actionability](./actionability.md) checks, focuses the element, fills it and triggers an `input` event after filling. Note that you can pass an empty string to clear the input field. + +If the target element is not an ``, ``); + await page.$eval('textarea', t => t.readOnly = true); + const input1 = page.locator('#input1'); + expect(await input1.isEditable()).toBe(false); + expect(await page.isEditable('#input1')).toBe(false); + const input2 = page.locator('#input2'); + expect(await input2.isEditable()).toBe(true); + expect(await page.isEditable('#input2')).toBe(true); + const textarea = page.locator('textarea'); + expect(await textarea.isEditable()).toBe(false); + expect(await page.isEditable('textarea')).toBe(false); +}); + +it('isChecked should work', async ({page}) => { + await page.setContent(`
Not a checkbox
`); + const element = page.locator('input'); + expect(await element.isChecked()).toBe(true); + expect(await page.isChecked('input')).toBe(true); + await element.evaluate(input => (input as HTMLInputElement).checked = false); + expect(await element.isChecked()).toBe(false); + expect(await page.isChecked('input')).toBe(false); + const error = await page.isChecked('div').catch(e => e); + expect(error.message).toContain('Not a checkbox or radio button'); +}); diff --git a/tests/page/locator-evaluate.spec.ts b/tests/page/locator-evaluate.spec.ts new file mode 100644 index 0000000000..c206befeaa --- /dev/null +++ b/tests/page/locator-evaluate.spec.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test as it, expect } from './pageTest'; + +it('should work', async ({page, server}) => { + await page.setContent('
10
'); + const tweet = page.locator('.tweet .like'); + const content = await tweet.evaluate(node => (node as HTMLElement).innerText); + expect(content).toBe('100'); +}); + +it('should retrieve content from subtree', async ({page, server}) => { + const htmlContent = '
not-a-child-div
a-child-div
'; + await page.setContent(htmlContent); + const elementHandle = page.locator('#myId .a'); + const content = await elementHandle.evaluate(node => (node as HTMLElement).innerText); + expect(content).toBe('a-child-div'); +}); + +it('should work for all', async ({page, server}) => { + await page.setContent('
'); + const tweet = page.locator('.tweet .like'); + const content = await tweet.evaluateAll(nodes => nodes.map(n => (n as HTMLElement).innerText)); + expect(content).toEqual(['100', '10']); +}); + +it('should retrieve content from subtree for all', async ({page, server}) => { + const htmlContent = '
not-a-child-div
a1-child-div
a2-child-div
'; + await page.setContent(htmlContent); + const element = page.locator('#myId .a'); + const content = await element.evaluateAll(nodes => nodes.map(n => (n as HTMLElement).innerText)); + expect(content).toEqual(['a1-child-div', 'a2-child-div']); +}); + +it('should not throw in case of missing selector for all', async ({page, server}) => { + const htmlContent = '
not-a-child-div
'; + await page.setContent(htmlContent); + const element = page.locator('#myId .a'); + const nodesLength = await element.evaluateAll(nodes => nodes.length); + expect(nodesLength).toBe(0); +}); diff --git a/tests/page/locator-misc-1.spec.ts b/tests/page/locator-misc-1.spec.ts new file mode 100644 index 0000000000..ab87912609 --- /dev/null +++ b/tests/page/locator-misc-1.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { contextTest as it, expect } from '../config/browserTest'; +import path from 'path'; + +it('should hover', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + const button = page.locator('#button-6'); + await button.hover(); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); +}); + +it('should hover when Node is removed', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => delete window['Node']); + const button = page.locator('#button-6'); + await button.hover(); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); +}); + +it('should fill input', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const handle = page.locator('input'); + await handle.fill('some value'); + expect(await page.evaluate(() => window['result'])).toBe('some value'); +}); + +it('should fill input when Node is removed', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => delete window['Node']); + const handle = page.locator('input'); + await handle.fill('some value'); + expect(await page.evaluate(() => window['result'])).toBe('some value'); +}); + +it('should check the box', async ({ page }) => { + await page.setContent(``); + const input = page.locator('input'); + await input.check(); + expect(await page.evaluate('checkbox.checked')).toBe(true); +}); + +it('should uncheck the box', async ({ page }) => { + await page.setContent(``); + const input = page.locator('input'); + await input.uncheck(); + expect(await page.evaluate('checkbox.checked')).toBe(false); +}); + +it('should select single option', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/select.html'); + const select = page.locator('select'); + await select.selectOption('blue'); + expect(await page.evaluate(() => window['result'].onInput)).toEqual(['blue']); + expect(await page.evaluate(() => window['result'].onChange)).toEqual(['blue']); +}); + +it('should focus a button', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = page.locator('button'); + expect(await button.evaluate(button => document.activeElement === button)).toBe(false); + await button.focus(); + expect(await button.evaluate(button => document.activeElement === button)).toBe(true); +}); + +it('should dispatch click event via ElementHandles', async ({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = page.locator('button'); + await button.dispatchEvent('click'); + expect(await page.evaluate(() => window['result'])).toBe('Clicked'); +}); + +it('should upload the file', async ({page, server, asset}) => { + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), asset('file-to-upload.txt')); + const input = page.locator('input'); + await input.setInputFiles(filePath); + expect(await page.evaluate(e => (e as HTMLInputElement).files[0].name, await input.first())).toBe('file-to-upload.txt'); +}); + +it.describe('tap group', () => { + it.use({ hasTouch: true }); + it('should send all of the correct events', async ({ page }) => { + await page.setContent(` +
a
+
b
+ `); + await page.locator('#a').tap(); + await page.locator('#b').tap(); + }); +}); diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts new file mode 100644 index 0000000000..ca17392788 --- /dev/null +++ b/tests/page/locator-misc-2.spec.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test as it, expect } from './pageTest'; + +it('should press', async ({ page }) => { + await page.setContent(``); + await page.locator('input').press('h'); + expect(await page.$eval('input', input => input.value)).toBe('h'); +}); + +it('should scroll into view', async ({ page, server, isAndroid }) => { + it.fixme(isAndroid); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + for (let i = 0; i < 11; ++i) { + const button = page.locator('#btn' + i); + const before = await button.evaluate(button => { + return button.getBoundingClientRect().right - window.innerWidth; + }); + expect(before).toBe(10 * i); + await button.scrollIntoViewIfNeeded(); + const after = await button.evaluate(button => { + return button.getBoundingClientRect().right - window.innerWidth; + }); + expect(after <= 0).toBe(true); + await page.evaluate(() => window.scrollTo(0, 0)); + } +}); + +it('should select textarea', async ({ page, server, browserName }) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = page.locator('textarea'); + await textarea.evaluate(textarea => (textarea as HTMLTextAreaElement).value = 'some value'); + await textarea.selectText(); + if (browserName === 'firefox') { + expect(await textarea.evaluate(el => (el as HTMLTextAreaElement).selectionStart)).toBe(0); + expect(await textarea.evaluate(el => (el as HTMLTextAreaElement).selectionEnd)).toBe(10); + } else { + expect(await page.evaluate(() => window.getSelection().toString())).toBe('some value'); + } +}); + +it('should type', async ({ page }) => { + await page.setContent(``); + await page.locator('input').type('hello'); + expect(await page.$eval('input', input => input.value)).toBe('hello'); +}); + +it('should wait for visible', async ({ page }) => { + async function giveItAChanceToResolve() { + for (let i = 0; i < 5; i++) + await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + } + + await page.setContent(``); + const div = page.locator('div'); + let done = false; + const promise = div.waitFor({ state: 'visible' }).then(() => done = true); + await giveItAChanceToResolve(); + expect(done).toBe(false); + await page.evaluate(() => (window as any).div.style.display = 'block'); + await promise; +}); + +it('should wait for already visible', async ({ page }) => { + await page.setContent(`
content
`); + const div = page.locator('div'); + await div.waitFor({ state: 'visible' }); +}); + +it('should take screenshot', async ({ page, server, browserName, headless, isAndroid }) => { + it.skip(browserName === 'firefox' && !headless); + it.skip(isAndroid, 'Different dpr. Remove after using 1x scale for screenshots.'); + await page.setViewportSize({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => window.scrollBy(50, 100)); + const element = page.locator('.box:nth-of-type(3)'); + const screenshot = await element.screenshot(); + expect(screenshot).toMatchSnapshot('screenshot-element-bounding-box.png'); +}); + +it('should return bounding box', async ({ page, server, browserName, headless }) => { + it.fail(browserName === 'firefox' && !headless); + + await page.setViewportSize({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const element = page.locator('.box:nth-of-type(13)'); + const box = await element.boundingBox(); + expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 }); +}); diff --git a/tests/page/locator-misc-2.spec.ts-snapshots/screenshot-element-bounding-box-chromium.png b/tests/page/locator-misc-2.spec.ts-snapshots/screenshot-element-bounding-box-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c3ddca298aba5c502f56e6656fd9330220b327 GIT binary patch literal 474 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^TK3NDj4wz}vwUf`e5)Ukjg=c4B?ZY6Q{gD+<_wP}}l z6nSQ36>Zn#-f|$iNz7fE$&pKHm-_rCzvuN??>N7ATC?@xO>6j=CvYp+J%0ba_|K}- z?e1;A{tEc13fvXy$m4X`&ax<)>7s7qi)jue-U{}mHHEE$Jc%sMXWq*OW$s-$i%G;W zZF||t6r&}VGkq>ExtSx>>!xYDf7J|T5r=j2<5pbF65(PsvM%I1RJ=tim8p?oS*Dfk z^|OT?x8ELn{&}OJ?ag94p-v0itCJs3c=3Z{t=G@fKZ902`4Zw^|LI!P4gZAOW-CLy zZnhPjNK*3L8l^h>>?RwlH95zZzB$)Wuj{urPJRCQ;w>U!mP`4PSezL|x?Qg=R|`2? z63iqS9eMQe#}9S&%O8Ea|IFgamuF(Pw=sImn|nEL`|j&|;`G&T&-U|Y;!@~!TUip(bP*}7q&3SPEgSM@h9ZZ`&x!)qR}^g{rtKT7+DOSu6{1- HoD!M<45`mj literal 0 HcmV?d00001 diff --git a/tests/page/locator-query-selector.spec.ts b/tests/page/locator-query-selector.spec.ts new file mode 100644 index 0000000000..c9b711fc9b --- /dev/null +++ b/tests/page/locator-query-selector.spec.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test as it, expect } from './pageTest'; + +it('should query existing element', async ({page, server}) => { + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent('
A
'); + const html = page.locator('html'); + const second = html.locator('.second'); + const inner = second.locator('.inner'); + const content = await page.evaluate(e => e.textContent, await inner.first()); + expect(content).toBe('A'); +}); + +it('should query existing elements', async ({page, server}) => { + await page.setContent('
A

B
'); + const html = page.locator('html'); + const elements = await html.locator('div').all(); + expect(elements.length).toBe(2); + const promises = elements.map(element => page.evaluate(e => e.textContent, element)); + expect(await Promise.all(promises)).toEqual(['A', 'B']); +}); + +it('should return empty array for non-existing elements', async ({page, server}) => { + await page.setContent('A
B'); + const html = page.locator('html'); + const elements = await html.locator('div').all(); + expect(elements.length).toBe(0); +}); + + +it('xpath should query existing element', async ({page, server}) => { + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent('
A
'); + const html = page.locator('html'); + const second = html.locator(`xpath=./body/div[contains(@class, 'second')]`); + const inner = second.locator(`xpath=./div[contains(@class, 'inner')]`); + const content = await page.evaluate(e => e.textContent, await inner.first()); + expect(content).toBe('A'); +}); + +it('xpath should return null for non-existing element', async ({page, server}) => { + await page.setContent('
B
'); + const html = page.locator('html'); + const second = await html.locator(`xpath=/div[contains(@class, 'third')]`).all(); + expect(second).toEqual([]); +}); diff --git a/types/structs.d.ts b/types/structs.d.ts index 7d1e981d74..c55ef43a0b 100644 --- a/types/structs.d.ts +++ b/types/structs.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JSHandle, ElementHandle, Frame, Page, BrowserContext } from './types'; +import { JSHandle, ElementHandle, Frame, Page, BrowserContext, Locator } from './types'; /** * Can be converted to JSON diff --git a/types/types.d.ts b/types/types.d.ts index 038adb8265..5091874460 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1934,6 +1934,18 @@ 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. + * + * Note that locator always implies visibility, so it will always be locating visible elements. + * + * Shortcut for main frame's [frame.locator(selector)](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. + */ + locator(selector: string): Locator; + /** * The page's main frame. Page is guaranteed to have a main frame which persists during navigations. */ @@ -4019,6 +4031,16 @@ export interface Frame { timeout?: number; }): 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. + * + * Note that locator always implies visibility, so it will always be locating visible elements. + * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + */ + locator(selector: string): Locator; + /** * Returns frame's name attribute as specified in the tag. * @@ -6611,6 +6633,1123 @@ export interface ElementHandle extends JSHandle { timeout?: number; }): Promise;} +/** + * Locator represents a view to the element(s) on the page. It captures the logic sufficient to retrieve the element at any + * given moment. Locator can be created with the + * [page.locator(selector)](https://playwright.dev/docs/api/class-page#page-locator) method. + * + * The difference between the Locator and [ElementHandle] is that the latter points to a particular element, while Locator + * only captures the logic of how to retrieve an element at any given moment. + * + * In the example below, handle points to a particular DOM element on page. If that element changes text or is used by + * React to render an entirely different component, handle is still pointing to that very DOM element. + * + * ```js + * const handle = await page.$('text=Submit'); + * // ... + * await handle.hover(); + * await handle.click(); + * ``` + * + * With the locator, every time the `element` is used, corresponding DOM element is located in the page using given + * selector. So in the snippet below, underlying DOM element is going to be located twice, using the given selector. + * + * ```js + * const element = page.locator('text=Submit'); + * // ... + * await element.hover(); + * await element.click(); + * ``` + * + */ +export interface Locator { + /** + * Resolves given locator to the first VISIBLE matching DOM element. If no elements matching the query are visible, waits + * for them up to a given timeout. + * @param options + */ + first(options?: { + timeout?: number; + }): Promise>; + /** + * Resolves given locator to all matching DOM elements. + */ + all(): Promise[]>; + /** + * Returns the return value of `pageFunction`. + * + * This method passes this handle as the first argument to `pageFunction`. + * + * If `pageFunction` returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return its + * value. + * + * Examples: + * + * ```js + * const tweets = await page.locator('.tweet .retweets'); + * expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets'); + * ``` + * + * @param pageFunction Function to be evaluated in the page context. + * @param arg Optional argument to pass to `pageFunction`. + * @param options + */ + evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; + evaluate(pageFunction: PageFunctionOn): Promise; + /** + * The method finds all elements matching the specified locator and passes an array of matched elements as a first argument + * to `pageFunction`. Returns the result of `pageFunction` invocation. + * + * If `pageFunction` returns a [Promise], then [`Locator.evaluateAll`] would wait for the promise to resolve and return its + * value. + * + * Examples: + * + * ```js + * const elements = page.locator('div'); + * const divCounts = await elements.evaluateAll((divs, min) => divs.length >= min, 10); + * ``` + * + * @param pageFunction Function to be evaluated in the page context. + * @param arg Optional argument to pass to `pageFunction`. + */ + evaluateAll(pageFunction: PageFunctionOn<(SVGElement | HTMLElement)[], Arg, R>, arg: Arg): Promise; + evaluateAll(pageFunction: PageFunctionOn<(SVGElement | HTMLElement)[], void, R>): Promise; + /** + * This method returns the bounding box of the element, or `null` if the element is not visible. The bounding box is + * calculated relative to the main frame viewport - which is usually the same as the browser window. + * + * Scrolling affects the returned bonding box, similarly to + * [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). That + * means `x` and/or `y` may be negative. + * + * Elements from child frames return the bounding box relative to the main frame, unlike the + * [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). + * + * Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the following + * snippet should click the center of the element. + * + * ```js + * const box = await element.boundingBox(); + * await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + * ``` + * + * @param options + */ + boundingBox(options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + }): Promise; + + /** + * This method checks the element by performing the following steps: + * 1. Ensure that element is a checkbox or a radio input. If not, this method throws. If the element is already checked, + * this method returns immediately. + * 1. Wait for [actionability](https://playwright.dev/docs/actionability) checks on the element, unless `force` option is set. + * 1. Scroll the element into view if needed. + * 1. Use [page.mouse](https://playwright.dev/docs/api/class-page#page-mouse) to click in the center of the element. + * 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. + * 1. Ensure that the element is now checked. If not, this method throws. + * + * If the element is detached from the DOM at any moment during the action, this method throws. + * + * When all steps combined have not finished during the specified `timeout`, this method throws a [TimeoutError]. Passing + * zero timeout disables this. + * @param options + */ + check(options?: { + /** + * Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`. + */ + force?: boolean; + + /** + * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can + * opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to + * inaccessible pages. Defaults to `false`. + */ + noWaitAfter?: boolean; + + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + + /** + * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults to + * `false`. Useful to wait until the element is ready for the action without performing it. + */ + trial?: boolean; + }): Promise; + + /** + * This method clicks the element by performing the following steps: + * 1. Wait for [actionability](https://playwright.dev/docs/actionability) checks on the element, unless `force` option is set. + * 1. Scroll the element into view if needed. + * 1. Use [page.mouse](https://playwright.dev/docs/api/class-page#page-mouse) to click in the center of the element, or + * the specified `position`. + * 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. + * + * If the element is detached from the DOM at any moment during the action, this method throws. + * + * When all steps combined have not finished during the specified `timeout`, this method throws a [TimeoutError]. Passing + * zero timeout disables this. + * @param options + */ + click(options?: { + /** + * Defaults to `left`. + */ + button?: "left"|"right"|"middle"; + + /** + * defaults to 1. See [UIEvent.detail]. + */ + clickCount?: number; + + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0. + */ + delay?: number; + + /** + * Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`. + */ + force?: boolean; + + /** + * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current + * modifiers back. If not specified, currently pressed modifiers are used. + */ + modifiers?: Array<"Alt"|"Control"|"Meta"|"Shift">; + + /** + * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can + * opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to + * inaccessible pages. Defaults to `false`. + */ + noWaitAfter?: boolean; + + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + + /** + * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults to + * `false`. Useful to wait until the element is ready for the action without performing it. + */ + trial?: boolean; + }): Promise; + + /** + * This method double clicks the element by performing the following steps: + * 1. Wait for [actionability](https://playwright.dev/docs/actionability) checks on the element, unless `force` option is set. + * 1. Scroll the element into view if needed. + * 1. Use [page.mouse](https://playwright.dev/docs/api/class-page#page-mouse) to double click in the center of the + * element, or the specified `position`. + * 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. Note that if the + * first click of the `dblclick()` triggers a navigation event, this method will throw. + * + * If the element is detached from the DOM at any moment during the action, this method throws. + * + * When all steps combined have not finished during the specified `timeout`, this method throws a [TimeoutError]. Passing + * zero timeout disables this. + * + * > NOTE: `element.dblclick()` dispatches two `click` events and a single `dblclick` event. + * @param options + */ + dblclick(options?: { + /** + * Defaults to `left`. + */ + button?: "left"|"right"|"middle"; + + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0. + */ + delay?: number; + + /** + * Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`. + */ + force?: boolean; + + /** + * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current + * modifiers back. If not specified, currently pressed modifiers are used. + */ + modifiers?: Array<"Alt"|"Control"|"Meta"|"Shift">; + + /** + * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can + * opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to + * inaccessible pages. Defaults to `false`. + */ + noWaitAfter?: boolean; + + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + + /** + * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults to + * `false`. Useful to wait until the element is ready for the action without performing it. + */ + trial?: boolean; + }): Promise; + + /** + * The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + * `click` is dispatched. This is equivalent to calling + * [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). + * + * ```js + * await element.dispatchEvent('click'); + * ``` + * + * Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties + * and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. + * + * Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + * - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + * - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) + * - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) + * - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) + * - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) + * - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) + * - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + * + * You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: + * + * ```js + * // Note you can only create DataTransfer in Chromium and Firefox + * const dataTransfer = await page.evaluateHandle(() => new DataTransfer()); + * await element.dispatchEvent('dragstart', { dataTransfer }); + * ``` + * + * @param type DOM event type: `"click"`, `"dragstart"`, etc. + * @param eventInit Optional event-specific initialization properties. + * @param options + */ + dispatchEvent(type: string, eventInit?: EvaluationArgument, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + }): Promise; + + /** + * Returns the return value of `pageFunction` as a [JSHandle]. + * + * This method passes this handle as the first argument to `pageFunction`. + * + * The only difference between + * [locator.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate) and + * [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle) + * is that + * [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle) + * returns [JSHandle]. + * + * If the function passed to the + * [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle) + * returns a [Promise], then + * [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle) + * would wait for the promise to resolve and return its value. + * + * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for more + * details. + * @param pageFunction Function to be evaluated in the page context. + * @param arg Optional argument to pass to `pageFunction`. + * @param options + */ + evaluateHandle(pageFunction: Function|string, arg?: EvaluationArgument, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + }): Promise; + + /** + * This method waits for [actionability](https://playwright.dev/docs/actionability) checks, focuses the element, fills it and triggers an `input` + * event after filling. Note that you can pass an empty string to clear the input field. + * + * If the target element is not an ``, `