test: F4.9 the editor by finger in Chromium and on an iPad upright and on its side

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>
This commit is contained in:
2026-10-04 13:02:59 +00:00
co-authored by Claude Opus 5.5
parent b8b999fd66
commit bd2e725791
7 changed files with 300 additions and 5 deletions
+85
View File
@@ -0,0 +1,85 @@
// 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', []);
},
};
}