The touch test runs in Chromium on the desktop and in WebKit as an iPad
(810x1080 and 1080x810): the acceptance of F4 as a script ("zwei
Rechnernetze" built anew by finger, set up and pinged across), two fingers
zoom the surface and never the page (M7, without a frame), every action
without a right click, every target at least 44 px. Real gestures: CDP in
Chromium, WebKit's own Input.dispatchTouchEvent through Playwright's
in-process connection (Playwright pinned to 1.62.1). A dragged finger holds
still before it is lifted, or Chromium takes it for a fling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
4.1 KiB
TypeScript
86 lines
4.1 KiB
TypeScript
// Fingers on the screen, in Chromium and in WebKit alike (plan §7.2, M7): a
|
|
// tap through Playwright's touchscreen; a press, a drag and two fingers
|
|
// through each engine's own touch input — Chromium's DevTools protocol
|
|
// (Input.dispatchTouchEvent), and WebKit's page protocol, whose command has
|
|
// the same name and points but which Playwright does not hand out: it is
|
|
// reached through the in-process connection (Playwright runs its server in
|
|
// the test's process). That seam belongs to Playwright's insides; the
|
|
// version is pinned (package.json), and a newer one may move it.
|
|
|
|
import type { CDPSession, Page } from '@playwright/test';
|
|
|
|
export interface Point {
|
|
readonly x: number;
|
|
readonly y: number;
|
|
}
|
|
|
|
type TouchType = 'touchStart' | 'touchMove' | 'touchEnd';
|
|
|
|
export interface Fingers {
|
|
tap(at: Point): Promise<void>;
|
|
/** One finger held where it is, then lifted (a long press: 700 ms). */
|
|
press(at: Point, milliseconds?: number): Promise<void>;
|
|
/** One finger moved from one point to another in steps. */
|
|
drag(from: Point, to: Point, steps?: number): Promise<void>;
|
|
/** Two fingers either side of a middle, moved from one distance apart to another. */
|
|
pinch(middle: Point, from: number, to: number, steps?: number): Promise<void>;
|
|
}
|
|
|
|
interface WebKitSession {
|
|
send(method: string, params: unknown): Promise<unknown>;
|
|
}
|
|
|
|
/** WebKit's page session: Playwright's own object behind the page, its page proxy's session. */
|
|
function webKitSession(page: Page): WebKitSession {
|
|
const connection = (page as unknown as { _connection: { toImpl?: (object: unknown) => { delegate?: unknown; _delegate?: unknown } } })._connection;
|
|
const server = connection.toImpl?.(page);
|
|
const delegate = (server?.delegate ?? server?._delegate) as { _pageProxySession?: WebKitSession } | undefined;
|
|
if (delegate?._pageProxySession === undefined) throw new Error("WebKit's page session is out of reach: Playwright's insides have changed");
|
|
return delegate._pageProxySession;
|
|
}
|
|
|
|
export async function fingers(page: Page): Promise<Fingers> {
|
|
const engine = page.context().browser()?.browserType().name();
|
|
let send: (type: TouchType, points: readonly Point[]) => Promise<unknown>;
|
|
if (engine === 'webkit') {
|
|
const session = webKitSession(page);
|
|
// WebKit ends a touch with the fingers lifted, not with none.
|
|
let last: readonly Point[] = [];
|
|
send = (type, points) => {
|
|
const touched = type === 'touchEnd' ? last : points;
|
|
if (type !== 'touchEnd') last = points;
|
|
return session.send('Input.dispatchTouchEvent', { type, touchPoints: touched.map(({ x, y }, id) => ({ x: Math.round(x), y: Math.round(y), id })) });
|
|
};
|
|
} else {
|
|
const cdp: CDPSession = await page.context().newCDPSession(page);
|
|
send = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(({ x, y }, id) => ({ x, y, id })) });
|
|
}
|
|
const steps = (count: number) => Array.from({ length: count }, (_, index) => (index + 1) / count);
|
|
return {
|
|
tap: (at) => page.touchscreen.tap(at.x, at.y),
|
|
async press(at, milliseconds = 700) {
|
|
await send('touchStart', [at]);
|
|
await page.waitForTimeout(milliseconds);
|
|
await send('touchEnd', []);
|
|
},
|
|
async drag(from, to, count = 8) {
|
|
await send('touchStart', [from]);
|
|
for (const share of steps(count)) await send('touchMove', [{ x: from.x + (to.x - from.x) * share, y: from.y + (to.y - from.y) * share }]);
|
|
// Held still before it is lifted, as a finger that puts something down: lifted on the move, Chromium
|
|
// takes it for a fling, and the next tap only stops the fling (no click).
|
|
await page.waitForTimeout(150);
|
|
await send('touchEnd', []);
|
|
},
|
|
async pinch(middle, from, to, count = 4) {
|
|
const pair = (distance: number): Point[] => [
|
|
{ x: middle.x - distance, y: middle.y },
|
|
{ x: middle.x + distance, y: middle.y },
|
|
];
|
|
await send('touchStart', pair(from));
|
|
for (const share of steps(count)) await send('touchMove', pair(from + (to - from) * share));
|
|
await page.waitForTimeout(150);
|
|
await send('touchEnd', []);
|
|
},
|
|
};
|
|
}
|