From f9d0cb35538a28358b627d7b37978468ca33e5e1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 14:31:59 +0100 Subject: [PATCH] test(app): exercise toast placement through host disconnection Observe the public reconnecting state with host connectivity interrupted externally. Keep native bounds and software keyboard assertions while removing runtime evaluation and private timeline-owner mutations. --- .../e2e/mobile/timeline-status/placement.mjs | 82 ++++++------------- 1 file changed, 25 insertions(+), 57 deletions(-) diff --git a/packages/app/e2e/mobile/timeline-status/placement.mjs b/packages/app/e2e/mobile/timeline-status/placement.mjs index 99beb86c8a..8317c9e152 100644 --- a/packages/app/e2e/mobile/timeline-status/placement.mjs +++ b/packages/app/e2e/mobile/timeline-status/placement.mjs @@ -1,21 +1,17 @@ -// Open an existing connected compact chat in a dev client, with the keyboard closed. -// node placement.mjs +// Open an existing compact chat, with the keyboard closed, then interrupt its host connection. +// Hold that interruption externally (for example, stop a disposable SSH TCP forward). +// Leave the daemon running. This test observes the public reconnecting state. +// node placement.mjs // coordinate-scale is screenshot pixels per layout point (Android density; iOS snapshots use 1). import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; -const [, , session, platform, target, serverId, coordinateScale, composerLabel, artifacts] = - process.argv; +const [, , session, platform, coordinateScale, composerLabel, artifacts] = process.argv; assert( - session && - ["android", "ios"].includes(platform) && - target && - serverId && - composerLabel && - artifacts, - "Pass all seven arguments", + session && ["android", "ios"].includes(platform) && composerLabel && artifacts, + "Pass all five arguments", ); const scale = Number(coordinateScale); assert(scale > 0, "Pass a positive coordinate scale"); @@ -26,61 +22,33 @@ function ad(...args) { timeout: 90_000, }); } -function evaluate(expression) { - const result = ad("cdp", "runtime", "eval", "--expr", expression); - assert(!result.includes("Runtime evaluation failed"), result); -} function capture(name) { const snapshot = ad("snapshot", "--raw", "--json"); writeFileSync(path.join(artifacts, `${name}.json`), snapshot); ad("screenshot", path.join(artifacts, `${name}.png`)); const nodes = JSON.parse(snapshot).data.nodes; const header = nodes.find((node) => node.identifier === "floating-header"); - const toast = nodes.find((node) => node.identifier === "agent-updating-toast"); - assert(header?.rect && toast?.rect, "Measured header and updating toast must be present"); + const toast = nodes.find((node) => node.identifier === "agent-reconnecting-toast"); + assert(header?.rect && toast?.rect, "Measured header and reconnecting toast must be present"); assert(toast.rect.height > 0 && toast.rect.width > 0, "Toast must have visible bounds"); const gap = (toast.rect.y - header.rect.y - header.rect.height) / scale; assert(Math.abs(gap - 12) <= 1, `${name}: actual header-to-toast gap ${gap}, expected12 points`); return toast.rect.y / scale; } -ad("wait", `label=${JSON.stringify(composerLabel)}`, "20000"); -ad("cdp", "target", "select", target); -try { - // Hold completion of the real timeline response. No response data is synthesized. - evaluate(` - globalThis.__statusPlacement = {}; - (() => { - const module = name => __r(Array.from(__r.getModules()).find(([,v]) => v.verboseName === name)[0]); - const store = module("src/runtime/host-runtime.ts").getHostRuntimeStore(); - const owner = module("src/stores/session-store.ts").useSessionStore.getState().sessions[${JSON.stringify(serverId)}].viewedTimelineSync; - const original = store.fetchAgentTimeline; - const releases = []; - let held = true; - store.fetchAgentTimeline = async function(...args) { - const response = await original.apply(this, args); - if (held) await new Promise(resolve => releases.push(resolve)); - return response; - }; - __statusPlacement.restore = () => { held = false; store.fetchAgentTimeline = original; releases.forEach(resolve => resolve()); }; - owner.setActive(false); - owner.setActive(true); - })(); - `); - ad("wait", 'id="agent-updating-toast"', "15000"); - const closedTop = capture("keyboard-closed"); - ad("press", `label=${JSON.stringify(composerLabel)}`); - const opened = ad("snapshot", "--raw", "--json"); - const nodes = JSON.parse(opened).data.nodes; - assert( - nodes.some( - (node) => /keyboard/i.test(node.type ?? "") || /inputmethod/i.test(node.bundleId ?? ""), - ), - "Use a real visible software keyboard; open Android sessions with --no-test-ime", - ); - const openTop = capture("keyboard-open"); - assert(Math.abs(openTop - closedTop) <= 1, "Keyboard must not translate the toast"); - console.log("PASS: rendered toast stays12 points below measured header, keyboard closed/open"); -} finally { - evaluate("globalThis.__statusPlacement?.restore?.(); delete globalThis.__statusPlacement;"); -} +ad("wait", 'id="agent-reconnecting-toast"', "20000"); +const closedTop = capture("keyboard-closed"); +ad("press", `label=${JSON.stringify(composerLabel)}`); +const opened = ad("snapshot", "--raw", "--json"); +const nodes = JSON.parse(opened).data.nodes; +assert( + nodes.some( + (node) => /keyboard/i.test(node.type ?? "") || /inputmethod/i.test(node.bundleId ?? ""), + ), + "Use a real visible software keyboard; open Android sessions with --no-test-ime", +); +const openTop = capture("keyboard-open"); +assert(Math.abs(openTop - closedTop) <= 1, "Keyboard must not translate the toast"); +console.log( + "PASS: rendered reconnecting toast stays12 points below measured header, keyboard closed/open", +);