feat: tap (#4097)
This commit is contained in:
Родитель
ebf207b7a1
Коммит
92dda698f8
92
docs/api.md
92
docs/api.md
|
@ -18,6 +18,7 @@
|
|||
- [class: FileChooser](#class-filechooser)
|
||||
- [class: Keyboard](#class-keyboard)
|
||||
- [class: Mouse](#class-mouse)
|
||||
- [class: Touchscreen](#class-touchscreen)
|
||||
- [class: Request](#class-request)
|
||||
- [class: Response](#class-response)
|
||||
- [class: Selectors](#class-selectors)
|
||||
|
@ -782,8 +783,10 @@ page.removeListener('request', logRequest);
|
|||
- [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders)
|
||||
- [page.setInputFiles(selector, files[, options])](#pagesetinputfilesselector-files-options)
|
||||
- [page.setViewportSize(viewportSize)](#pagesetviewportsizeviewportsize)
|
||||
- [page.tap(selector[, options])](#pagetapselector-options)
|
||||
- [page.textContent(selector[, options])](#pagetextcontentselector-options)
|
||||
- [page.title()](#pagetitle)
|
||||
- [page.touchscreen](#pagetouchscreen)
|
||||
- [page.type(selector, text[, options])](#pagetypeselector-text-options)
|
||||
- [page.uncheck(selector, [options])](#pageuncheckselector-options)
|
||||
- [page.unroute(url[, handler])](#pageunrouteurl-handler)
|
||||
|
@ -1833,6 +1836,31 @@ await page.setViewportSize({
|
|||
await page.goto('https://example.com');
|
||||
```
|
||||
|
||||
#### page.tap(selector[, options])
|
||||
- `selector` <[string]> A selector to search for element to tap. If there are multiple elements satisfying the selector, the first will be tapped. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
- `position` <[Object]> A point to tap relative to the top-left corner of element padding box. If not specified, taps some visible point of the element.
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the tap, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
|
||||
- `noWaitAfter` <[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`.
|
||||
- `force` <[boolean]> Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`.
|
||||
- `timeout` <[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)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
|
||||
- returns: <[Promise]> Promise that resolves when the element matching `selector` is successfully tapped.
|
||||
|
||||
This method taps an element matching `selector` by performing the following steps:
|
||||
1. Find an element match matching `selector`. If there is none, wait until a matching element is attached to the DOM.
|
||||
1. Wait for [actionability](./actionability.md) checks on the matched element, unless `force` option is set. If the element is detached during the checks, the whole action is retried.
|
||||
1. Scroll the element into view if needed.
|
||||
1. Use [page.touchscreen](#pagemouse) to tap the center of the element, or the specified `position`.
|
||||
1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set.
|
||||
|
||||
When all steps combined have not finished during the specified `timeout`, this method rejects with a [TimeoutError]. Passing zero timeout disables this.
|
||||
|
||||
> **NOTE** `page.tap()` requires that the `hasTouch` option of the browser context be set to true.
|
||||
|
||||
Shortcut for [page.mainFrame().tap()](#framename).
|
||||
|
||||
#### page.textContent(selector[, options])
|
||||
- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
|
@ -1841,13 +1869,14 @@ await page.goto('https://example.com');
|
|||
|
||||
Resolves to the `element.textContent`.
|
||||
|
||||
|
||||
#### page.title()
|
||||
- returns: <[Promise]<[string]>> The page's title.
|
||||
|
||||
Shortcut for [page.mainFrame().title()](#frametitle).
|
||||
|
||||
#### page.touchscreen
|
||||
|
||||
- returns: <[Touchscreen]>
|
||||
|
||||
#### page.type(selector, text[, options])
|
||||
- `selector` <[string]> A selector of an element to type into. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](#working-with-selectors) for more details.
|
||||
|
@ -2154,6 +2183,7 @@ console.log(text);
|
|||
- [frame.selectOption(selector, values[, options])](#frameselectoptionselector-values-options)
|
||||
- [frame.setContent(html[, options])](#framesetcontenthtml-options)
|
||||
- [frame.setInputFiles(selector, files[, options])](#framesetinputfilesselector-files-options)
|
||||
- [frame.tap(selector[, options])](#frametapselector-options)
|
||||
- [frame.textContent(selector[, options])](#frametextcontentselector-options)
|
||||
- [frame.title()](#frametitle)
|
||||
- [frame.type(selector, text[, options])](#frametypeselector-text-options)
|
||||
|
@ -2594,6 +2624,29 @@ This method expects `selector` to point to an [input element](https://developer.
|
|||
|
||||
Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files.
|
||||
|
||||
#### frame.tap(selector[, options])
|
||||
- `selector` <[string]> A selector to search for element to tap. If there are multiple elements satisfying the selector, the first will be tapped. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
- `position` <[Object]> A point to tap relative to the top-left corner of element padding box. If not specified, taps some visible point of the element.
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the tap, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
|
||||
- `noWaitAfter` <[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`.
|
||||
- `force` <[boolean]> Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`.
|
||||
- `timeout` <[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)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
|
||||
- returns: <[Promise]> Promise that resolves when the element matching `selector` is successfully tapped.
|
||||
|
||||
This method taps an element matching `selector` by performing the following steps:
|
||||
1. Find an element match matching `selector`. If there is none, wait until a matching element is attached to the DOM.
|
||||
1. Wait for [actionability](./actionability.md) checks on the matched element, unless `force` option is set. If the element is detached during the checks, the whole action is retried.
|
||||
1. Scroll the element into view if needed.
|
||||
1. Use [page.touchscreen](#pagemouse) to tap the center of the element, or the specified `position`.
|
||||
1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set.
|
||||
|
||||
When all steps combined have not finished during the specified `timeout`, this method rejects with a [TimeoutError]. Passing zero timeout disables this.
|
||||
|
||||
> **NOTE** `frame.tap()` requires that the `hasTouch` option of the browser context be set to true.
|
||||
|
||||
#### frame.textContent(selector[, options])
|
||||
- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
|
@ -2602,7 +2655,6 @@ Sets the value of the file input to these file paths or files. If some of the `f
|
|||
|
||||
Resolves to the `element.textContent`.
|
||||
|
||||
|
||||
#### frame.title()
|
||||
- returns: <[Promise]<[string]>> The page's title.
|
||||
|
||||
|
@ -2800,6 +2852,7 @@ ElementHandle instances can be used as an argument in [`page.$eval()`](#pageeval
|
|||
- [elementHandle.selectOption(values[, options])](#elementhandleselectoptionvalues-options)
|
||||
- [elementHandle.selectText([options])](#elementhandleselecttextoptions)
|
||||
- [elementHandle.setInputFiles(files[, options])](#elementhandlesetinputfilesfiles-options)
|
||||
- [elementHandle.tap([options])](#elementhandletapoptions)
|
||||
- [elementHandle.textContent()](#elementhandletextcontent)
|
||||
- [elementHandle.toString()](#elementhandletostring)
|
||||
- [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
||||
|
@ -3119,6 +3172,29 @@ This method expects `elementHandle` to point to an [input element](https://devel
|
|||
|
||||
Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files.
|
||||
|
||||
#### elementHandle.tap([options])
|
||||
- `options` <[Object]>
|
||||
- `position` <[Object]> A point to tap relative to the top-left corner of element padding box. If not specified, taps some visible point of the element.
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the tap, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
|
||||
- `force` <[boolean]> Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`.
|
||||
- `noWaitAfter` <[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`.
|
||||
- `timeout` <[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)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
|
||||
- returns: <[Promise]> Promise that resolves when the element is successfully tapped.
|
||||
|
||||
This method taps the element by performing the following steps:
|
||||
1. Wait for [actionability](./actionability.md) checks on the element, unless `force` option is set.
|
||||
1. Scroll the element into view if needed.
|
||||
1. Use [page.touchscreen](#pagemouse) to tap 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 rejects.
|
||||
|
||||
When all steps combined have not finished during the specified `timeout`, this method rejects with a [TimeoutError]. Passing zero timeout disables this.
|
||||
|
||||
> **NOTE** `elementHandle.tap()` requires that the `hasTouch` option of the browser context be set to true.
|
||||
|
||||
#### elementHandle.textContent()
|
||||
- returns: <[Promise]<[null]|[string]>> Resolves to the `node.textContent`.
|
||||
|
||||
|
@ -3701,6 +3777,17 @@ Dispatches a `mousemove` event.
|
|||
|
||||
Dispatches a `mouseup` event.
|
||||
|
||||
### class: Touchscreen
|
||||
|
||||
The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
|
||||
touchscreen can only be used in browser contexts that have been intialized with `hasTouch` set to true.
|
||||
|
||||
#### touchscreen.tap(x, y)
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- returns: <[Promise]>
|
||||
|
||||
Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`).
|
||||
|
||||
### class: Request
|
||||
|
||||
|
@ -4839,6 +4926,7 @@ const { chromium } = require('playwright');
|
|||
[Selectors]: #class-selectors "Selectors"
|
||||
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable"
|
||||
[TimeoutError]: #class-timeouterror "TimeoutError"
|
||||
[Touchscreen]: #class-touchscreen "Touchscreen"
|
||||
[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail"
|
||||
[URL]: https://nodejs.org/api/url.html
|
||||
[USKeyboardLayout]: ../src/usKeyboardLayout.ts "USKeyboardLayout"
|
||||
|
|
|
@ -27,7 +27,7 @@ export { FileChooser } from './fileChooser';
|
|||
export { Logger } from './types';
|
||||
export { TimeoutError } from '../utils/errors';
|
||||
export { Frame } from './frame';
|
||||
export { Keyboard, Mouse } from './input';
|
||||
export { Keyboard, Mouse, Touchscreen } from './input';
|
||||
export { JSHandle } from './jsHandle';
|
||||
export { Request, Response, Route } from './network';
|
||||
export { Page } from './page';
|
||||
|
|
|
@ -115,6 +115,12 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
|
|||
});
|
||||
}
|
||||
|
||||
async tap(options: channels.ElementHandleTapOptions = {}): Promise<void> {
|
||||
return this._wrapApiCall('elementHandle.tap', async () => {
|
||||
return await this._elementChannel.tap(options);
|
||||
});
|
||||
}
|
||||
|
||||
async selectOption(values: string | ElementHandle | SelectOption | string[] | ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise<string[]> {
|
||||
return this._wrapApiCall('elementHandle.selectOption', async () => {
|
||||
const result = await this._elementChannel.selectOption({ ...convertSelectOptionValues(values), ...options });
|
||||
|
|
|
@ -322,6 +322,12 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
|||
});
|
||||
}
|
||||
|
||||
async tap(selector: string, options: channels.FrameTapOptions = {}) {
|
||||
return this._wrapApiCall(this._apiName('tap'), async () => {
|
||||
return await this._channel.tap({ selector, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async fill(selector: string, value: string, options: channels.FrameFillOptions = {}) {
|
||||
return this._wrapApiCall(this._apiName('fill'), async () => {
|
||||
return await this._channel.fill({ selector, value, ...options });
|
||||
|
|
|
@ -72,3 +72,15 @@ export class Mouse {
|
|||
await this.click(x, y, { ...options, clickCount: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
export class Touchscreen {
|
||||
private _channel: channels.PageChannel;
|
||||
|
||||
constructor(channel: channels.PageChannel) {
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
async tap(x: number, y: number) {
|
||||
await this._channel.touchscreenTap({x, y});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import { Download } from './download';
|
|||
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
||||
import { Worker } from './worker';
|
||||
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
|
||||
import { Keyboard, Mouse } from './input';
|
||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||
import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult, JSHandle } from './jsHandle';
|
||||
import { Request, Response, Route, RouteHandler, validateHeaders } from './network';
|
||||
import { FileChooser } from './fileChooser';
|
||||
|
@ -77,6 +77,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
|||
readonly accessibility: Accessibility;
|
||||
readonly keyboard: Keyboard;
|
||||
readonly mouse: Mouse;
|
||||
readonly touchscreen: Touchscreen;
|
||||
coverage: ChromiumCoverage | null = null;
|
||||
pdf?: (options?: PDFOptions) => Promise<Buffer>;
|
||||
|
||||
|
@ -102,6 +103,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
|||
this.accessibility = new Accessibility(this._channel);
|
||||
this.keyboard = new Keyboard(this._channel);
|
||||
this.mouse = new Mouse(this._channel);
|
||||
this.touchscreen = new Touchscreen(this._channel);
|
||||
|
||||
this._mainFrame = Frame.from(initializer.mainFrame);
|
||||
this._mainFrame._page = this;
|
||||
|
@ -490,6 +492,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
|||
return this._attributeToPage(() => this._mainFrame.dblclick(selector, options));
|
||||
}
|
||||
|
||||
async tap(selector: string, options?: channels.FrameTapOptions) {
|
||||
return this._attributeToPage(() => this._mainFrame.tap(selector, options));
|
||||
}
|
||||
|
||||
async fill(selector: string, value: string, options?: channels.FrameFillOptions) {
|
||||
return this._attributeToPage(() => this._mainFrame.fill(selector, value, options));
|
||||
}
|
||||
|
|
|
@ -92,6 +92,12 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
|
|||
}, { ...metadata, type: 'dblclick', target: this._elementHandle, page: this._elementHandle._page });
|
||||
}
|
||||
|
||||
async tap(params: channels.ElementHandleTapParams, metadata?: channels.Metadata): Promise<void> {
|
||||
return runAction(async controller => {
|
||||
return await this._elementHandle.tap(controller, params);
|
||||
}, { ...metadata, type: 'tap', target: this._elementHandle, page: this._elementHandle._page });
|
||||
}
|
||||
|
||||
async selectOption(params: channels.ElementHandleSelectOptionParams, metadata?: channels.Metadata): Promise<channels.ElementHandleSelectOptionResult> {
|
||||
return runAction(async controller => {
|
||||
const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle);
|
||||
|
|
|
@ -125,6 +125,12 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
|
|||
}, { ...metadata, type: 'dblclick', target: params.selector, page: this._frame._page });
|
||||
}
|
||||
|
||||
async tap(params: channels.FrameTapParams, metadata?: channels.Metadata): Promise<void> {
|
||||
return runAction(async controller => {
|
||||
return await this._frame.tap(controller, params.selector, params);
|
||||
}, { ...metadata, type: 'tap', target: params.selector, page: this._frame._page });
|
||||
}
|
||||
|
||||
async fill(params: channels.FrameFillParams, metadata?: channels.Metadata): Promise<void> {
|
||||
return runAction(async controller => {
|
||||
return await this._frame.fill(controller, params.selector, params.value, params);
|
||||
|
|
|
@ -185,6 +185,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
|
|||
await this._page.mouse.click(params.x, params.y, params);
|
||||
}
|
||||
|
||||
async touchscreenTap(params: channels.PageTouchscreenTapParams): Promise<void> {
|
||||
await this._page.touchscreen.tap(params.x, params.y);
|
||||
}
|
||||
|
||||
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams): Promise<channels.PageAccessibilitySnapshotResult> {
|
||||
const rootAXNode = await this._page.accessibility.snapshot({
|
||||
interestingOnly: params.interestingOnly,
|
||||
|
|
|
@ -710,6 +710,7 @@ export interface PageChannel extends Channel {
|
|||
mouseDown(params: PageMouseDownParams, metadata?: Metadata): Promise<PageMouseDownResult>;
|
||||
mouseUp(params: PageMouseUpParams, metadata?: Metadata): Promise<PageMouseUpResult>;
|
||||
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
|
||||
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
|
||||
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
|
||||
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
|
||||
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
|
||||
|
@ -996,6 +997,14 @@ export type PageMouseClickOptions = {
|
|||
clickCount?: number,
|
||||
};
|
||||
export type PageMouseClickResult = void;
|
||||
export type PageTouchscreenTapParams = {
|
||||
x: number,
|
||||
y: number,
|
||||
};
|
||||
export type PageTouchscreenTapOptions = {
|
||||
|
||||
};
|
||||
export type PageTouchscreenTapResult = void;
|
||||
export type PageAccessibilitySnapshotParams = {
|
||||
interestingOnly?: boolean,
|
||||
root?: ElementHandleChannel,
|
||||
|
@ -1133,6 +1142,7 @@ export interface FrameChannel extends Channel {
|
|||
selectOption(params: FrameSelectOptionParams, metadata?: Metadata): Promise<FrameSelectOptionResult>;
|
||||
setContent(params: FrameSetContentParams, metadata?: Metadata): Promise<FrameSetContentResult>;
|
||||
setInputFiles(params: FrameSetInputFilesParams, metadata?: Metadata): Promise<FrameSetInputFilesResult>;
|
||||
tap(params: FrameTapParams, metadata?: Metadata): Promise<FrameTapResult>;
|
||||
textContent(params: FrameTextContentParams, metadata?: Metadata): Promise<FrameTextContentResult>;
|
||||
title(params?: FrameTitleParams, metadata?: Metadata): Promise<FrameTitleResult>;
|
||||
type(params: FrameTypeParams, metadata?: Metadata): Promise<FrameTypeResult>;
|
||||
|
@ -1475,6 +1485,28 @@ export type FrameSetInputFilesOptions = {
|
|||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type FrameSetInputFilesResult = void;
|
||||
export type FrameTapParams = {
|
||||
selector: string,
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameTapOptions = {
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
timeout?: number,
|
||||
};
|
||||
export type FrameTapResult = void;
|
||||
export type FrameTextContentParams = {
|
||||
selector: string,
|
||||
timeout?: number,
|
||||
|
@ -1675,6 +1707,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
|
|||
selectOption(params: ElementHandleSelectOptionParams, metadata?: Metadata): Promise<ElementHandleSelectOptionResult>;
|
||||
selectText(params: ElementHandleSelectTextParams, metadata?: Metadata): Promise<ElementHandleSelectTextResult>;
|
||||
setInputFiles(params: ElementHandleSetInputFilesParams, metadata?: Metadata): Promise<ElementHandleSetInputFilesResult>;
|
||||
tap(params: ElementHandleTapParams, metadata?: Metadata): Promise<ElementHandleTapResult>;
|
||||
textContent(params?: ElementHandleTextContentParams, metadata?: Metadata): Promise<ElementHandleTextContentResult>;
|
||||
type(params: ElementHandleTypeParams, metadata?: Metadata): Promise<ElementHandleTypeResult>;
|
||||
uncheck(params: ElementHandleUncheckParams, metadata?: Metadata): Promise<ElementHandleUncheckResult>;
|
||||
|
@ -1944,6 +1977,27 @@ export type ElementHandleSetInputFilesOptions = {
|
|||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type ElementHandleSetInputFilesResult = void;
|
||||
export type ElementHandleTapParams = {
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleTapOptions = {
|
||||
force?: boolean,
|
||||
noWaitAfter?: boolean,
|
||||
modifiers?: ('Alt' | 'Control' | 'Meta' | 'Shift')[],
|
||||
position?: {
|
||||
x: number,
|
||||
y: number,
|
||||
},
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleTapResult = void;
|
||||
export type ElementHandleTextContentParams = {};
|
||||
export type ElementHandleTextContentOptions = {};
|
||||
export type ElementHandleTextContentResult = {
|
||||
|
|
|
@ -775,6 +775,11 @@ Page:
|
|||
- middle
|
||||
clickCount: number?
|
||||
|
||||
touchscreenTap:
|
||||
parameters:
|
||||
x: number
|
||||
y: number
|
||||
|
||||
accessibilitySnapshot:
|
||||
parameters:
|
||||
interestingOnly: boolean?
|
||||
|
@ -1230,6 +1235,27 @@ Frame:
|
|||
timeout: number?
|
||||
noWaitAfter: boolean?
|
||||
|
||||
tap:
|
||||
parameters:
|
||||
selector: string
|
||||
force: boolean?
|
||||
noWaitAfter: boolean?
|
||||
modifiers:
|
||||
type: array?
|
||||
items:
|
||||
type: enum
|
||||
literals:
|
||||
- Alt
|
||||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
timeout: number?
|
||||
|
||||
textContent:
|
||||
parameters:
|
||||
selector: string
|
||||
|
@ -1626,6 +1652,26 @@ ElementHandle:
|
|||
timeout: number?
|
||||
noWaitAfter: boolean?
|
||||
|
||||
tap:
|
||||
parameters:
|
||||
force: boolean?
|
||||
noWaitAfter: boolean?
|
||||
modifiers:
|
||||
type: array?
|
||||
items:
|
||||
type: enum
|
||||
literals:
|
||||
- Alt
|
||||
- Control
|
||||
- Meta
|
||||
- Shift
|
||||
position:
|
||||
type: object?
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
timeout: number?
|
||||
|
||||
textContent:
|
||||
returns:
|
||||
value: string?
|
||||
|
|
|
@ -404,6 +404,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
button: tOptional(tEnum(['left', 'right', 'middle'])),
|
||||
clickCount: tOptional(tNumber),
|
||||
});
|
||||
scheme.PageTouchscreenTapParams = tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
});
|
||||
scheme.PageAccessibilitySnapshotParams = tObject({
|
||||
interestingOnly: tOptional(tBoolean),
|
||||
root: tOptional(tChannel('ElementHandle')),
|
||||
|
@ -589,6 +593,17 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
timeout: tOptional(tNumber),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
});
|
||||
scheme.FrameTapParams = tObject({
|
||||
selector: tString,
|
||||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.FrameTextContentParams = tObject({
|
||||
selector: tString,
|
||||
timeout: tOptional(tNumber),
|
||||
|
@ -767,6 +782,16 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
timeout: tOptional(tNumber),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
});
|
||||
scheme.ElementHandleTapParams = tObject({
|
||||
force: tOptional(tBoolean),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
|
||||
position: tOptional(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
})),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleTextContentParams = tOptional(tObject({}));
|
||||
scheme.ElementHandleTypeParams = tObject({
|
||||
text: tString,
|
||||
|
|
|
@ -59,7 +59,7 @@ export class Video {
|
|||
}
|
||||
|
||||
export type ActionMetadata = {
|
||||
type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload',
|
||||
type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload' | 'tap',
|
||||
page: Page,
|
||||
target?: dom.ElementHandle | string,
|
||||
value?: string,
|
||||
|
|
|
@ -131,3 +131,27 @@ export class RawMouseImpl implements input.RawMouse {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RawTouchscreenImpl implements input.RawTouchscreen {
|
||||
private _client: CRSession;
|
||||
|
||||
constructor(client: CRSession) {
|
||||
this._client = client;
|
||||
}
|
||||
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
|
||||
await Promise.all([
|
||||
this._client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
modifiers: toModifiersMask(modifiers),
|
||||
touchPoints: [{
|
||||
x, y
|
||||
}]
|
||||
}),
|
||||
this._client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
modifiers: toModifiersMask(modifiers),
|
||||
touchPoints: []
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crP
|
|||
import * as dialog from '../dialog';
|
||||
import { PageDelegate } from '../page';
|
||||
import * as path from 'path';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './crInput';
|
||||
import { RawMouseImpl, RawKeyboardImpl, RawTouchscreenImpl } from './crInput';
|
||||
import { getAccessibilityTree } from './crAccessibility';
|
||||
import { CRCoverage } from './crCoverage';
|
||||
import { CRPDF } from './crPdf';
|
||||
|
@ -49,6 +49,7 @@ export class CRPage implements PageDelegate {
|
|||
readonly _page: Page;
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||
readonly _targetId: string;
|
||||
readonly _opener: CRPage | null;
|
||||
private readonly _pdf: CRPDF;
|
||||
|
@ -69,6 +70,7 @@ export class CRPage implements PageDelegate {
|
|||
this._opener = opener;
|
||||
this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac);
|
||||
this.rawMouse = new RawMouseImpl(client);
|
||||
this.rawTouchscreen = new RawTouchscreenImpl(client);
|
||||
this._pdf = new CRPDF(client);
|
||||
this._coverage = new CRCoverage(client);
|
||||
this._browserContext = browserContext;
|
||||
|
|
|
@ -400,6 +400,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
async tap(controller: ProgressController, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return controller.run(async progress => {
|
||||
const result = await this._tap(progress, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
_tap(progress: Progress, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
|
||||
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), options);
|
||||
}
|
||||
|
||||
async selectOption(controller: ProgressController, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
|
||||
return controller.run(async progress => {
|
||||
const result = await this._selectOption(progress, elements, values, options);
|
||||
|
|
|
@ -141,3 +141,18 @@ export class RawMouseImpl implements input.RawMouse {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RawTouchscreenImpl implements input.RawTouchscreen {
|
||||
private _client: FFSession;
|
||||
|
||||
constructor(client: FFSession) {
|
||||
this._client = client;
|
||||
}
|
||||
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
|
||||
await this._client.send('Page.dispatchTapEvent', {
|
||||
x,
|
||||
y,
|
||||
modifiers: toModifiersMask(modifiers),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { getAccessibilityTree } from './ffAccessibility';
|
|||
import { FFBrowserContext } from './ffBrowser';
|
||||
import { FFSession, FFSessionEvents } from './ffConnection';
|
||||
import { FFExecutionContext } from './ffExecutionContext';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
|
||||
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput';
|
||||
import { FFNetworkManager } from './ffNetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||
|
@ -38,6 +38,7 @@ export class FFPage implements PageDelegate {
|
|||
readonly cspErrorsAsynchronousForInlineScipts = true;
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||
readonly _session: FFSession;
|
||||
readonly _page: Page;
|
||||
readonly _networkManager: FFNetworkManager;
|
||||
|
@ -55,6 +56,7 @@ export class FFPage implements PageDelegate {
|
|||
this._opener = opener;
|
||||
this.rawKeyboard = new RawKeyboardImpl(session);
|
||||
this.rawMouse = new RawMouseImpl(session);
|
||||
this.rawTouchscreen = new RawTouchscreenImpl(session);
|
||||
this._contextIdToContext = new Map();
|
||||
this._browserContext = browserContext;
|
||||
this._page = new Page(this, browserContext);
|
||||
|
|
|
@ -820,6 +820,12 @@ export class Frame extends EventEmitter {
|
|||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async tap(controller: ProgressController, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
return controller.run(async progress => {
|
||||
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._tap(progress, options)));
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async fill(controller: ProgressController, selector: string, value: string, options: types.NavigatingActionWaitOptions) {
|
||||
return controller.run(async progress => {
|
||||
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options)));
|
||||
|
|
|
@ -293,3 +293,24 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface RawTouchscreen {
|
||||
tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
|
||||
}
|
||||
|
||||
export class Touchscreen {
|
||||
private _raw: RawTouchscreen;
|
||||
private _page: Page;
|
||||
|
||||
constructor(raw: RawTouchscreen, page: Page) {
|
||||
this._raw = raw;
|
||||
this._page = page;
|
||||
}
|
||||
|
||||
async tap(x: number, y: number) {
|
||||
if (!this._page._browserContext._options.hasTouch)
|
||||
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
|
||||
await this._raw.tap(x, y, this._page.keyboard._modifiers());
|
||||
await this._page._doSlowMo();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import { Selectors } from './selectors';
|
|||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
readonly rawKeyboard: input.RawKeyboard;
|
||||
readonly rawTouchscreen: input.RawTouchscreen;
|
||||
|
||||
opener(): Promise<Page | null>;
|
||||
|
||||
|
@ -127,6 +128,7 @@ export class Page extends EventEmitter {
|
|||
readonly _browserContext: BrowserContext;
|
||||
readonly keyboard: input.Keyboard;
|
||||
readonly mouse: input.Mouse;
|
||||
readonly touchscreen: input.Touchscreen;
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
readonly _delegate: PageDelegate;
|
||||
readonly _state: PageState;
|
||||
|
@ -161,6 +163,7 @@ export class Page extends EventEmitter {
|
|||
this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate));
|
||||
this.keyboard = new input.Keyboard(delegate.rawKeyboard, this);
|
||||
this.mouse = new input.Mouse(delegate.rawMouse, this);
|
||||
this.touchscreen = new input.Touchscreen(delegate.rawTouchscreen, this);
|
||||
this._timeoutSettings = new TimeoutSettings(browserContext._timeoutSettings);
|
||||
this._screenshotter = new Screenshotter(this);
|
||||
this._frameManager = new frames.FrameManager(this);
|
||||
|
|
|
@ -127,3 +127,19 @@ export class RawMouseImpl implements input.RawMouse {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RawTouchscreenImpl implements input.RawTouchscreen {
|
||||
private readonly _pageProxySession: WKSession;
|
||||
|
||||
constructor(session: WKSession) {
|
||||
this._pageProxySession = session;
|
||||
}
|
||||
|
||||
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
|
||||
await this._pageProxySession.send('Input.dispatchTapEvent', {
|
||||
x,
|
||||
y,
|
||||
modifiers: toModifiersMask(modifiers),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import { getAccessibilityTree } from './wkAccessibility';
|
|||
import { WKBrowserContext } from './wkBrowser';
|
||||
import { WKSession } from './wkConnection';
|
||||
import { WKExecutionContext } from './wkExecutionContext';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './wkInput';
|
||||
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput';
|
||||
import { WKInterceptableRequest } from './wkInterceptableRequest';
|
||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
import { WKWorkers } from './wkWorkers';
|
||||
|
@ -44,6 +44,7 @@ const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
|||
export class WKPage implements PageDelegate {
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||
_session: WKSession;
|
||||
private _provisionalPage: WKProvisionalPage | null = null;
|
||||
readonly _page: Page;
|
||||
|
@ -74,6 +75,7 @@ export class WKPage implements PageDelegate {
|
|||
this._opener = opener;
|
||||
this.rawKeyboard = new RawKeyboardImpl(pageProxySession);
|
||||
this.rawMouse = new RawMouseImpl(pageProxySession);
|
||||
this.rawTouchscreen = new RawTouchscreenImpl(pageProxySession);
|
||||
this._contextIdToContext = new Map();
|
||||
this._page = new Page(this, browserContext);
|
||||
this._workers = new WKWorkers(this._page);
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* 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 { expect, folio } from './fixtures';
|
||||
import { ElementHandle } from '..';
|
||||
import type { ServerResponse } from 'http';
|
||||
|
||||
const fixtures = folio.extend();
|
||||
fixtures.page.override(async ({browser}, runTest) => {
|
||||
const page = await browser.newPage({
|
||||
hasTouch: true
|
||||
});
|
||||
await runTest(page);
|
||||
await page.close();
|
||||
});
|
||||
const { it } = fixtures.build();
|
||||
|
||||
it('should send all of the correct events', async ({page}) => {
|
||||
await page.setContent(`
|
||||
<div id="a" style="background: lightblue; width: 50px; height: 50px">a</div>
|
||||
<div id="b" style="background: pink; width: 50px; height: 50px">b</div>
|
||||
`);
|
||||
await page.tap('#a');
|
||||
const eventsHandle = await trackEvents(await page.$('#b'));
|
||||
await page.tap('#b');
|
||||
// webkit doesnt send pointerenter or pointerleave or mouseout
|
||||
expect(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointerover', 'pointerenter',
|
||||
'pointerdown', 'touchstart',
|
||||
'pointerup', 'pointerout',
|
||||
'pointerleave', 'touchend',
|
||||
'mouseover', 'mouseenter',
|
||||
'mousemove', 'mousedown',
|
||||
'mouseup', 'click',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not send mouse events touchstart is canceled', async ({page}) => {
|
||||
await page.setContent('hello world');
|
||||
await page.evaluate(() => {
|
||||
// touchstart is not cancelable unless passive is false
|
||||
document.addEventListener('touchstart', t => t.preventDefault(), {passive: false});
|
||||
});
|
||||
const eventsHandle = await trackEvents(await page.$('body'));
|
||||
await page.tap('body');
|
||||
expect(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointerover', 'pointerenter',
|
||||
'pointerdown', 'touchstart',
|
||||
'pointerup', 'pointerout',
|
||||
'pointerleave', 'touchend',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not send mouse events when touchend is canceled', async ({page}) => {
|
||||
await page.setContent('hello world');
|
||||
await page.evaluate(() => {
|
||||
document.addEventListener('touchend', t => t.preventDefault());
|
||||
});
|
||||
const eventsHandle = await trackEvents(await page.$('body'));
|
||||
await page.tap('body');
|
||||
expect(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointerover', 'pointerenter',
|
||||
'pointerdown', 'touchstart',
|
||||
'pointerup', 'pointerout',
|
||||
'pointerleave', 'touchend',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should wait for a navigation caused by a tap', async ({server, page}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<a href="/intercept-this.html">link</a>;
|
||||
`);
|
||||
const responsePromise = new Promise<ServerResponse>(resolve => {
|
||||
server.setRoute('/intercept-this.html', (handler, response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
let resolved = false;
|
||||
const tapPromise = page.tap('a').then(() => resolved = true);
|
||||
const response = await responsePromise;
|
||||
// make sure the tap doesnt resolve too early
|
||||
await new Promise(x => setTimeout(x, 100));
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
response.end('foo');
|
||||
|
||||
await tapPromise;
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with modifiers', async ({page}) => {
|
||||
await page.setContent('hello world');
|
||||
const altKeyPromise = page.evaluate(() => new Promise(resolve => {
|
||||
document.addEventListener('touchstart', event => {
|
||||
resolve(event.altKey);
|
||||
}, {passive: false});
|
||||
}));
|
||||
// make sure the evals hit the page
|
||||
await page.evaluate(() => void 0);
|
||||
await page.tap('body', {
|
||||
modifiers: ['Alt']
|
||||
});
|
||||
expect(await altKeyPromise).toBe(true);
|
||||
});
|
||||
|
||||
it('should send well formed touch points', async ({page}) => {
|
||||
const promises = Promise.all([
|
||||
page.evaluate(() => new Promise(resolve => {
|
||||
document.addEventListener('touchstart', event => {
|
||||
resolve([...event.touches].map(t => ({
|
||||
identifier: t.identifier,
|
||||
clientX: t.clientX,
|
||||
clientY: t.clientY,
|
||||
pageX: t.pageX,
|
||||
pageY: t.pageY,
|
||||
radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],
|
||||
radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],
|
||||
rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],
|
||||
force: 'force' in t ? t.force : t['webkitForce'],
|
||||
})));
|
||||
}, false);
|
||||
})),
|
||||
page.evaluate(() => new Promise(resolve => {
|
||||
document.addEventListener('touchend', event => {
|
||||
resolve([...event.touches].map(t => ({
|
||||
identifier: t.identifier,
|
||||
clientX: t.clientX,
|
||||
clientY: t.clientY,
|
||||
pageX: t.pageX,
|
||||
pageY: t.pageY,
|
||||
radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],
|
||||
radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],
|
||||
rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],
|
||||
force: 'force' in t ? t.force : t['webkitForce'],
|
||||
})));
|
||||
}, false);
|
||||
})),
|
||||
]);
|
||||
// make sure the evals hit the page
|
||||
await page.evaluate(() => void 0);
|
||||
await page.touchscreen.tap(40, 60);
|
||||
const [touchstart, touchend] = await promises;
|
||||
|
||||
expect(touchstart).toEqual([{
|
||||
clientX: 40,
|
||||
clientY: 60,
|
||||
force: 1,
|
||||
identifier: 0,
|
||||
pageX: 40,
|
||||
pageY: 60,
|
||||
radiusX: 1,
|
||||
radiusY: 1,
|
||||
rotationAngle: 0,
|
||||
}]);
|
||||
expect(touchend).toEqual([]);
|
||||
});
|
||||
|
||||
it('should wait until an element is visible to tap it', async ({page}) => {
|
||||
const div = await page.evaluateHandle(() => {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'not clicked';
|
||||
document.body.appendChild(button);
|
||||
button.style.display = 'none';
|
||||
return button;
|
||||
});
|
||||
const tapPromise = div.tap();
|
||||
await div.evaluate(div => div.onclick = () => div.textContent = 'clicked');
|
||||
await div.evaluate(div => div.style.display = 'block');
|
||||
await tapPromise;
|
||||
expect(await div.textContent()).toBe('clicked');
|
||||
});
|
||||
|
||||
async function trackEvents(target: ElementHandle) {
|
||||
const eventsHandle = await target.evaluateHandle(target => {
|
||||
const events: string[] = [];
|
||||
for (const event of [
|
||||
'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click',
|
||||
'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup',
|
||||
'touchstart', 'touchend', 'touchmove', 'touchcancel',
|
||||
])
|
||||
target.addEventListener(event, () => events.push(event), false);
|
||||
return events;
|
||||
});
|
||||
return eventsHandle;
|
||||
}
|
Загрузка…
Ссылка в новой задаче