// 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; /** One finger held where it is, then lifted (a long press: 700 ms). */ press(at: Point, milliseconds?: number): Promise; /** One finger moved from one point to another in steps. */ drag(from: Point, to: Point, steps?: number): Promise; /** Two fingers either side of a middle, moved from one distance apart to another. */ pinch(middle: Point, from: number, to: number, steps?: number): Promise; } interface WebKitSession { send(method: string, params: unknown): Promise; } /** 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 { const engine = page.context().browser()?.browserType().name(); let send: (type: TouchType, points: readonly Point[]) => Promise; 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', []); }, }; }