mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat: introduce touchscreen.touch() for dispatching raw touch events (#31457)
This commit is contained in:
		
							parent
							
								
									33ac75b7ab
								
							
						
					
					
						commit
						a3e31fd2c4
					
				@ -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.
 | 
			
		||||
@ -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 });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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'])),
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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.');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/playwright-core/types/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								packages/playwright-core/types/types.d.ts
									
									
									
									
										vendored
									
									
								
							@ -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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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?
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										76
									
								
								tests/library/touch.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								tests/library/touch.spec.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user