From ec6393f31ff18ffc7052010e6c4b53f36380f32d Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:57:41 +0200 Subject: [PATCH] Keep pairing QR codes fully visible on mobile (#6758) * Keep pairing QR code within compact tile * Move pairing screenshot after viewport assertion * test(pairing): verify the complete QR code on Android The native regression failed with the fixed-size SVG because the code was clipped at both screen edges. Verify the complete square and visible margins through the Android app, and keep browser geometry assertions in the pairing helper. Co-authored-by: fdkevin <25277753+fdkevin0@users.noreply.github.com> --------- Co-authored-by: fdkevin <25277753+fdkevin0@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../app/e2e/browser/pair-device-relay.spec.ts | 19 ++++++++ .../app/e2e/mobile/pair-device/android.mjs | 43 +++++++++++++++++++ .../app/e2e/support/helpers/pair-device.ts | 21 +++++++++ .../components/pair-device-section.tsx | 8 +--- 4 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 packages/app/e2e/mobile/pair-device/android.mjs diff --git a/packages/app/e2e/browser/pair-device-relay.spec.ts b/packages/app/e2e/browser/pair-device-relay.spec.ts index 8c4e306a1e..dbd3814d38 100644 --- a/packages/app/e2e/browser/pair-device-relay.spec.ts +++ b/packages/app/e2e/browser/pair-device-relay.spec.ts @@ -1,6 +1,8 @@ import { test } from "../support/fixtures"; import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; import { + expectPairingOffer, + expectCompletePairingQr, expectRelayConsent, openPairDeviceModal, preparePairingHost, @@ -18,3 +20,20 @@ test("opens relay consent in browser web", async ({ page }) => { await daemon.close(); } }); + +test("keeps the pairing QR code inside its compact tile", async ({ page }, testInfo) => { + const daemon = await startIsolatedHostDaemon("pair-device-browser-compact", { + mutableRelay: { enabled: true }, + }); + try { + await preparePairingHost(page, daemon); + await page.setViewportSize({ width: 320, height: 568 }); + await openPairDeviceModal(page); + await expectPairingOffer(page); + + await expectCompletePairingQr(page); + await page.screenshot({ path: testInfo.outputPath("pairing-compact.png") }); + } finally { + await daemon.close(); + } +}); diff --git a/packages/app/e2e/mobile/pair-device/android.mjs b/packages/app/e2e/mobile/pair-device/android.mjs new file mode 100644 index 0000000000..7ab660c2b0 --- /dev/null +++ b/packages/app/e2e/mobile/pair-device/android.mjs @@ -0,0 +1,43 @@ +// Start on Settings > Host > Pair device with relay enabled in the Android app. +// node packages/app/e2e/mobile/pair-device/android.mjs +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; + +const [, , session] = process.argv; +assert(session, "Pass the agent-device session for the connected Android app"); + +function ad(...args) { + return execFileSync("agent-device", [...args, "--session", session], { + encoding: "utf8", + timeout: 60_000, + }); +} + +ad( + "press", + 'label="Pair a device, Scan a QR code or copy a link to connect your phone to this host"', + "--settle", +); +ad("wait", "text", "Pairing QR code", "10000"); +const { data } = JSON.parse(ad("snapshot", "-i", "--json")); +const sheet = data.nodes.find((node) => node.label === "Bottom Sheet"); +const qr = data.nodes.find((node) => node.label === "Pairing QR code"); +assert(sheet?.rect, "Pairing sheet must be visible"); +assert(qr?.rect, "Pairing QR code must be visible"); +assert(qr.rect.x > sheet.rect.x, "QR must have an unclipped left margin inside the sheet"); +assert( + qr.rect.x + qr.rect.width < sheet.rect.x + sheet.rect.width, + "QR must have an unclipped right margin inside the sheet", +); +assert(Math.abs(qr.rect.width - qr.rect.height) <= 1, "Complete QR must remain square"); +assert(qr.rect.y >= sheet.rect.y, "QR top must be visible inside the sheet"); +assert( + qr.rect.y + qr.rect.height <= sheet.rect.y + sheet.rect.height, + "QR bottom must be visible inside the sheet", +); +assert( + data.nodes.some((node) => node.label === "Copy" && node.enabled), + "Copy must remain available", +); +console.log("PASS: complete square QR fits inside the pairing sheet", qr.rect); +ad("press", 'label="Close"', "--settle"); diff --git a/packages/app/e2e/support/helpers/pair-device.ts b/packages/app/e2e/support/helpers/pair-device.ts index d8908015b8..46cdda5ed9 100644 --- a/packages/app/e2e/support/helpers/pair-device.ts +++ b/packages/app/e2e/support/helpers/pair-device.ts @@ -220,3 +220,24 @@ export function expectDaemonPidUnchanged( expect(before).toBeDefined(); expect(after).toBe(before); } + +export async function expectCompletePairingQr(page: Page): Promise { + const qr = page.getByRole("img", { name: "Pairing QR code" }); + await qr.scrollIntoViewIfNeeded(); + // IntersectionObserver rounds fractional sheet/scroll coordinates. + await expect(qr).toBeInViewport({ ratio: 0.999 }); + const bounds = await qr.evaluate((element) => { + const tile = element.parentElement; + if (!tile) throw new Error("Pairing QR tile is missing"); + const qrRect = element.getBoundingClientRect(); + const tileRect = tile.getBoundingClientRect(); + return { + qr: { x: qrRect.x, y: qrRect.y, right: qrRect.right, bottom: qrRect.bottom }, + tile: { x: tileRect.x, y: tileRect.y, right: tileRect.right, bottom: tileRect.bottom }, + }; + }); + expect(bounds.qr.x).toBeGreaterThanOrEqual(bounds.tile.x); + expect(bounds.qr.y).toBeGreaterThanOrEqual(bounds.tile.y); + expect(bounds.qr.right).toBeLessThanOrEqual(bounds.tile.right); + expect(bounds.qr.bottom).toBeLessThanOrEqual(bounds.tile.bottom); +} diff --git a/packages/app/src/desktop/components/pair-device-section.tsx b/packages/app/src/desktop/components/pair-device-section.tsx index 9209f6fcbf..982469008a 100644 --- a/packages/app/src/desktop/components/pair-device-section.tsx +++ b/packages/app/src/desktop/components/pair-device-section.tsx @@ -80,7 +80,6 @@ export function PairDeviceSection({ serverId, onClose }: PairDeviceSectionProps) type: "svg", errorCorrectionLevel: "M", margin: 1, - width: 480, }), enabled: Boolean(pairingQuery.data?.url), dataShape: "value", @@ -275,7 +274,8 @@ function PairingQr({ svg, isError }: { svg: string | null; isError: boolean }) { return ( @@ -365,10 +365,6 @@ const styles = StyleSheet.create((theme) => ({ borderColor: theme.colors.border, backgroundColor: theme.colors.palette.white, }, - qrImage: { - width: "100%", - height: "100%", - }, linkRow: { flexDirection: "row", alignItems: "center",