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",