This commit is contained in:
Joel Einbinder 2020-10-19 10:07:33 -07:00 коммит произвёл GitHub
Родитель ebf207b7a1
Коммит 92dda698f8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
24 изменённых файлов: 567 добавлений и 8 удалений

Просмотреть файл

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

198
test/tap.spec.ts Normal file
Просмотреть файл

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