test(terminal): reproduce duplicate Android hardware submissions

Exercise the native input through a real terminal and assert one carriage return for Enter and numpad Enter. The current implementation fails both cases; preserve the reproduction while native input ownership is decided.
This commit is contained in:
paseo-bot[bot]
2026-10-17 03:40:11 +02:00
parent 0fac80202e
commit 8c50c4a439
@@ -0,0 +1,78 @@
// Select a shell terminal in the Android app, loaded from the checkout under test.
// node packages/app/e2e/mobile/terminal-input/hardware-enter.android.mjs <session> <serial> <daemon-home> <terminal-id>
// Requires a local daemon, Node.js in the terminal, and agent-device with the real IME.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { setTimeout as delay } from "node:timers/promises";
const [, , session, serial, home, terminalId] = process.argv;
assert(
session && serial && home && terminalId,
"Pass session, adb serial, daemon home, terminal ID",
);
const cli = fileURLToPath(new URL("../../../../cli/dist/index.js", import.meta.url));
function terminal(...args) {
return execFileSync(process.execPath, [cli, "--home", home, "terminal", ...args], {
encoding: "utf8",
timeout: 10_000,
});
}
function key(code) {
execFileSync("adb", ["-s", serial, "shell", "input", "keyevent", code], { timeout: 10_000 });
}
function capture() {
return JSON.parse(terminal("capture", terminalId, "--json")).lines.map((line) => line.trim());
}
async function waitForLine(line) {
const deadline = Date.now() + 10_000;
do {
if (capture().includes(line)) return;
await delay(100);
} while (Date.now() < deadline);
assert.fail(`Terminal did not produce ${line}`);
}
// Inspect bytes at the PTY rather than counting prompts, which depend on the shell theme.
// Keep reading after Enter to catch its key-up event and to check the following character.
const reader = [
"process.stdin.setRawMode(true)",
"let hex = ''",
"console.log('REA' + 'DY')",
"process.stdin.on('data', data => {",
"if (data.includes(3)) { process.stdin.setRawMode(false); process.exit(0); }",
"hex += data.toString('hex'); console.log('HE' + 'X:' + hex);",
"})",
].join(";");
const quotedReader = "'" + reader.replaceAll("'", "'\\''") + "'";
const command = `node -e ${quotedReader}`;
const failures = [];
for (const enter of ["KEYCODE_ENTER", "KEYCODE_NUMPAD_ENTER"]) {
terminal("send-keys", terminalId, "--literal", command);
terminal("send-keys", terminalId, "Enter");
try {
await waitForLine("READY");
execFileSync("agent-device", ["--session", session, "press", 'id="terminal-surface"'], {
encoding: "utf8",
timeout: 30_000,
});
key(enter);
await waitForLine("HEX:0d");
// The second event is native key-up, so allow it to arrive before the assertion.
await delay(500);
assert(!capture().includes("HEX:0d0d"), `${enter} submitted twice`);
key("KEYCODE_X");
await waitForLine("HEX:0d78");
console.log(`PASS: ${enter} sends one carriage return and preserves the next character`);
} catch (error) {
failures.push(error);
console.error(error.message);
} finally {
terminal("send-keys", terminalId, "C-c");
await delay(300);
terminal("send-keys", terminalId, "--literal", "clear");
terminal("send-keys", terminalId, "Enter");
await delay(300);
}
}
assert.equal(failures.length, 0, "Hardware Enter regression");