evOo-local/apps/web/e2e/mobile-computer-keyboard.spec.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

105 lines
4.0 KiB
TypeScript

import { readFile } from "node:fs/promises";
import path from "node:path";
import { expect, test } from "@playwright/test";
import { captureScreenshot } from "./helpers";
const computerRoot = path.resolve(import.meta.dirname, "../../../infra/sandboxes/computer");
test("touch users can open and dismiss the remote computer keyboard", async ({
page,
}, testInfo) => {
const assets = new Map([
["/embed.html", await readFile(path.join(computerRoot, "embed.html"), "utf8")],
[
"/clipboard-bridge.js",
await readFile(path.join(computerRoot, "clipboard-bridge.js"), "utf8"),
],
["/mobile-keyboard.js", await readFile(path.join(computerRoot, "mobile-keyboard.js"), "utf8")],
[
"/core/rfb.js",
`export default class RFB {
constructor() {
this.viewOnly = false;
this.focusOnClick = true;
globalThis.__rfbKeys = [];
}
sendKey(...args) { globalThis.__rfbKeys.push(args); }
}`,
],
[
"/core/input/keyboard.js",
`export default class Keyboard {
constructor() { this.onkeyevent = null; }
grab() {}
ungrab() {}
}`,
],
["/core/input/keysym.js", "export default { XK_BackSpace: 0xff08 };"],
["/core/input/keysymdef.js", "export default { lookup: (codePoint) => codePoint };"],
]);
await page.addInitScript(() => {
Object.defineProperty(navigator, "maxTouchPoints", { configurable: true, value: 1 });
const viewport = new EventTarget();
Object.defineProperties(viewport, {
height: { configurable: true, value: 420 },
offsetTop: { configurable: true, value: 12 },
});
Object.defineProperty(window, "visualViewport", { configurable: true, value: viewport });
});
await page.setViewportSize({ width: 390, height: 844 });
await page.route("http://keyboard.test/**", async (route) => {
const pathname = new URL(route.request().url()).pathname;
const body = assets.get(pathname);
if (body == null) return route.fulfill({ status: 404, body: "Not found" });
return route.fulfill({
contentType: pathname.endsWith(".html") ? "text/html" : "text/javascript",
body,
});
});
await page.goto("http://keyboard.test/embed.html");
const keyboardButton = page.getByRole("button", { name: "Show keyboard" });
const trackpadButton = page.getByRole("button", { name: "Use trackpad" });
const keyboardInput = page.getByRole("textbox", { name: "Remote computer keyboard input" });
await expect(keyboardButton).toBeVisible();
await expect(trackpadButton).toBeVisible();
await trackpadButton.click();
await expect(page.getByRole("button", { name: "Use direct touch" })).toBeVisible();
await keyboardButton.click();
await expect(page.getByRole("button", { name: "Hide keyboard" })).toBeVisible();
await expect(keyboardInput).toBeFocused();
await expect(page.locator("html")).toHaveClass(/mobile-keyboard-open/);
await expect(page.locator("#screen")).toHaveCSS("height", "420px");
await expect
.poll(() =>
page.evaluate(() => ({
height: document.documentElement.style.getPropertyValue("--mobile-visual-height"),
top: document.documentElement.style.getPropertyValue("--mobile-visual-top"),
})),
)
.toEqual({ height: "420px", top: "12px" });
await keyboardInput.pressSequentially("mobile typing");
const forwardedKeysyms = await page.evaluate(() =>
Reflect.get(globalThis, "__rfbKeys").map(([keysym]: [number]) => keysym),
);
expect(forwardedKeysyms).toEqual(
Array.from("mobile typing", (character) => character.codePointAt(0)),
);
await captureScreenshot(page, testInfo, "mobile-computer-keyboard-open");
await page.getByRole("button", { name: "Hide keyboard" }).click();
await expect(keyboardButton).toBeVisible();
await expect(keyboardInput).not.toBeFocused();
await expect(page.locator("html")).not.toHaveClass(/mobile-keyboard-open/);
await expect
.poll(() =>
page.evaluate(() =>
document.documentElement.style.getPropertyValue("--mobile-visual-height"),
),
)
.toBe("");
});