feat: introduce touchscreen.touch() for dispatching raw touch events (#31457)

This commit is contained in:
Yury Semikhatsky 2024-06-27 14:37:36 -07:00 committed by GitHub
parent 33ac75b7ab
commit a3e31fd2c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 223 additions and 0 deletions

View File

@ -20,3 +20,23 @@ Dispatches a `touchstart` and `touchend` event with a single touch at the positi
### param: Touchscreen.tap.y
* since: v1.8
- `y` <[float]>
## async method: Touchscreen.touch
* since: v1.46
Synthesizes a touch event.
### param: Touchscreen.touch.type
* since: v1.46
- `type` <[TouchType]<"touchstart"|"touchend"|"touchmove"|"touchcancel">>
Type of the touch event.
### param: Touchscreen.touch.touches
* since: v1.46
- `touchPoints` <[Array]<[Object]>>
- `x` <[float]> x coordinate of the event in CSS pixels.
- `y` <[float]> y coordinate of the event in CSS pixels.
- `id` ?<[int]> Identifier used to track the touch point between events, must be unique within an event. Optional.
List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between touch events for the duration of its movement around the surface.

View File

@ -89,4 +89,8 @@ export class Touchscreen implements api.Touchscreen {
async tap(x: number, y: number) {
await this._page._channel.touchscreenTap({ x, y });
}
async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[]) {
await this._page._channel.touchscreenTouch({ type, touchPoints });
}
}

View File

@ -31,6 +31,7 @@ export const slowMoActions = new Set([
'Page.mouseClick',
'Page.mouseWheel',
'Page.touchscreenTap',
'Page.touchscreenTouch',
'Frame.blur',
'Frame.check',
'Frame.click',
@ -89,6 +90,7 @@ export const commandsWithTracingSnapshots = new Set([
'Page.mouseClick',
'Page.mouseWheel',
'Page.touchscreenTap',
'Page.touchscreenTouch',
'Frame.evalOnSelector',
'Frame.evalOnSelectorAll',
'Frame.addScriptTag',

View File

@ -1237,6 +1237,15 @@ scheme.PageTouchscreenTapParams = tObject({
y: tNumber,
});
scheme.PageTouchscreenTapResult = tOptional(tObject({}));
scheme.PageTouchscreenTouchParams = tObject({
type: tEnum(['touchstart', 'touchend', 'touchmove', 'touchcancel']),
touchPoints: tArray(tObject({
x: tNumber,
y: tNumber,
id: tOptional(tNumber),
})),
});
scheme.PageTouchscreenTouchResult = tOptional(tObject({}));
scheme.PageAccessibilitySnapshotParams = tObject({
interestingOnly: tOptional(tBoolean),
root: tOptional(tChannel(['ElementHandle'])),

View File

@ -179,4 +179,19 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
}),
]);
}
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
switch (eventType) {
case 'touchstart': type = 'touchStart'; break;
case 'touchmove': type = 'touchMove'; break;
case 'touchend': type = 'touchEnd'; break;
case 'touchcancel': type = 'touchCancel'; break;
default: throw new Error('Invalid eventType: ' + eventType);
}
await this._client.send('Input.dispatchTouchEvent', {
type,
touchPoints,
modifiers: toModifiersMask(modifiers)
});
}
}

View File

@ -265,6 +265,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await this._page.touchscreen.tap(params.x, params.y, metadata);
}
async touchscreenTouch(params: channels.PageTouchscreenTouchParams, metadata: CallMetadata) {
await this._page.touchscreen.touch(params.type, params.touchPoints, metadata);
}
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise<channels.PageAccessibilitySnapshotResult> {
const rootAXNode = await this._page.accessibility.snapshot({
interestingOnly: params.interestingOnly,

View File

@ -166,4 +166,8 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
modifiers: toModifiersMask(modifiers),
});
}
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
throw new Error('Not implemented yet.');
}
}

View File

@ -308,6 +308,7 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
export interface RawTouchscreen {
tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>): Promise<void>;
}
export class Touchscreen {
@ -326,4 +327,19 @@ export class Touchscreen {
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());
}
async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], metadata?: CallMetadata) {
if (metadata && touchPoints.length === 1)
metadata.point = { x: touchPoints[0].x, y: touchPoints[0].y };
if (!this._page._browserContext._options.hasTouch)
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
const ids = new Set<number>();
for (const point of touchPoints) {
if (point.id !== undefined) {
if (ids.has(point.id))
throw new Error(`Duplicate touch point id: ${point.id}`);
ids.add(point.id);
}
}
await this._raw.touch(type, touchPoints, this._page.keyboard._modifiers());
}
}

View File

@ -182,4 +182,20 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
modifiers: toModifiersMask(modifiers),
});
}
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
switch (eventType) {
case 'touchstart': type = 'touchStart'; break;
case 'touchmove': type = 'touchMove'; break;
case 'touchend': type = 'touchEnd'; break;
case 'touchcancel': type = 'touchCancel'; break;
default: throw new Error('Invalid eventType: ' + eventType);
}
await this._pageProxySession.send('Input.dispatchTouchEvent', {
type,
touchPoints: touchPoints.map(p => ({ ...p, id: p.id || 0 })),
modifiers: toModifiersMask(modifiers)
});
}
}

View File

@ -19682,6 +19682,29 @@ export interface Touchscreen {
* @param y
*/
tap(x: number, y: number): Promise<void>;
/**
* Synthesizes a touch event.
* @param type Type of the touch event.
* @param touchPoints List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between
* touch events for the duration of its movement around the surface.
*/
touch(type: "touchstart"|"touchend"|"touchmove"|"touchcancel", touchPoints: ReadonlyArray<{
/**
* x coordinate of the event in CSS pixels.
*/
x: number;
/**
* y coordinate of the event in CSS pixels.
*/
y: number;
/**
* Identifier used to track the touch point between events, must be unique within an event. Optional.
*/
id?: number;
}>): Promise<void>;
}
/**

View File

@ -1890,6 +1890,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
mouseClick(params: PageMouseClickParams, metadata?: CallMetadata): Promise<PageMouseClickResult>;
mouseWheel(params: PageMouseWheelParams, metadata?: CallMetadata): Promise<PageMouseWheelResult>;
touchscreenTap(params: PageTouchscreenTapParams, metadata?: CallMetadata): Promise<PageTouchscreenTapResult>;
touchscreenTouch(params: PageTouchscreenTouchParams, metadata?: CallMetadata): Promise<PageTouchscreenTouchResult>;
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: CallMetadata): Promise<PageAccessibilitySnapshotResult>;
pdf(params: PagePdfParams, metadata?: CallMetadata): Promise<PagePdfResult>;
startJSCoverage(params: PageStartJSCoverageParams, metadata?: CallMetadata): Promise<PageStartJSCoverageResult>;
@ -2257,6 +2258,18 @@ export type PageTouchscreenTapOptions = {
};
export type PageTouchscreenTapResult = void;
export type PageTouchscreenTouchParams = {
type: 'touchstart' | 'touchend' | 'touchmove' | 'touchcancel',
touchPoints: {
x: number,
y: number,
id?: number,
}[],
};
export type PageTouchscreenTouchOptions = {
};
export type PageTouchscreenTouchResult = void;
export type PageAccessibilitySnapshotParams = {
interestingOnly?: boolean,
root?: ElementHandleChannel,

View File

@ -1603,6 +1603,27 @@ Page:
slowMo: true
snapshot: true
touchscreenTouch:
parameters:
type:
type: enum
literals:
- touchstart
- touchend
- touchmove
- touchcancel
touchPoints:
type: array
items:
type: object
properties:
x: number
y: number
id: number?
flags:
slowMo: true
snapshot: true
accessibilitySnapshot:
parameters:
interestingOnly: boolean?

View File

@ -0,0 +1,76 @@
/**
* 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 type { Locator } from 'playwright-core';
it.use({ hasTouch: true });
it.fixme(({ browserName }) => browserName === 'firefox');
it('slow swipe events @smoke', async ({ page }) => {
it.fixme();
await page.setContent(`<div id="a" style="background: lightblue; width: 200px; height: 200px">a</div>`);
const eventsHandle = await trackEvents(await page.locator('#a'));
const center = await centerPoint(page.locator('#a'));
await page.touchscreen.touch('touchstart', [{ ...center, id: 1 }]);
expect.soft(await eventsHandle.jsonValue()).toEqual([
'pointerover',
'pointerenter',
'pointerdown',
'touchstart',
]);
await eventsHandle.evaluate(events => events.length = 0);
await page.touchscreen.touch('touchmove', [{ x: center.x + 10, y: center.y + 10, id: 1 }]);
await page.touchscreen.touch('touchmove', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
expect.soft(await eventsHandle.jsonValue()).toEqual([
'pointermove',
'touchmove',
'pointermove',
'touchmove',
]);
await eventsHandle.evaluate(events => events.length = 0);
await page.touchscreen.touch('touchend', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
expect.soft(await eventsHandle.jsonValue()).toEqual([
'pointerup',
'pointerout',
'pointerleave',
'touchend',
]);
});
async function trackEvents(target: Locator) {
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), { passive: false });
return events;
});
return eventsHandle;
}
async function centerPoint(e: Locator) {
const box = await e.boundingBox();
if (!box)
throw new Error('Element is not visible');
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
}