From 130cbc961f931cfac5ee0945ee6f777da191fcb1 Mon Sep 17 00:00:00 2001 From: paseo-bot Date: Tue, 22 Sep 2026 18:00:07 +0200 Subject: [PATCH] test(app): show the slash autocomplete popover losing its anchor Both tests fail on main. With the popover open, toggling the desktop sidebar moves the composer, and the popover keeps the offsets measured before the toggle: it lands 160px away from the composer, and when the sidebar expands it is pushed 90px past the right edge of a 1280px viewport. Evidence for #4872. --- ...ocomplete-popover-anchor-staleness.spec.ts | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 packages/app/e2e/browser/autocomplete-popover-anchor-staleness.spec.ts diff --git a/packages/app/e2e/browser/autocomplete-popover-anchor-staleness.spec.ts b/packages/app/e2e/browser/autocomplete-popover-anchor-staleness.spec.ts new file mode 100644 index 0000000000..42b2d744c7 --- /dev/null +++ b/packages/app/e2e/browser/autocomplete-popover-anchor-staleness.spec.ts @@ -0,0 +1,140 @@ +import { expect, test, type Page } from "../support/fixtures"; +import { composerLocator, expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; + +const TEST_COMMANDS = [ + { name: "help", description: "Show help", argumentHint: "" }, + { name: "tdd", description: "Red test first", argumentHint: "" }, + { name: "hello", description: "Greeting", argumentHint: "" }, +] as const; + +async function installListCommandsStub(page: Page): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const server = ws.connectToServer(); + ws.onMessage((message) => server.send(message)); + server.onMessage((message) => { + if (typeof message !== "string") { + ws.send(message); + return; + } + try { + const parsed = JSON.parse(message) as { + type?: string; + message?: { type?: string; payload?: { commands?: unknown; error?: string | null } }; + }; + if ( + parsed.type === "session" && + parsed.message?.type === "list_commands_response" && + parsed.message.payload + ) { + parsed.message.payload.commands = TEST_COMMANDS; + parsed.message.payload.error = null; + ws.send(JSON.stringify(parsed)); + return; + } + } catch { + // forward + } + ws.send(message); + }); + }); +} + +async function boxes(page: Page) { + const popover = await page.evaluate(() => { + const element = document.querySelector('[data-testid="composer-autocomplete-popover"]'); + if (!(element instanceof HTMLElement)) return null; + const rect = element.getBoundingClientRect(); + return { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height), + }; + }); + const composerBox = await page.getByTestId("message-input-root").boundingBox(); + return { + popover, + composer: composerBox + ? { + x: Math.round(composerBox.x), + y: Math.round(composerBox.y), + width: Math.round(composerBox.width), + height: Math.round(composerBox.height), + } + : null, + }; +} + +// Failing evidence for https://github.com/getpaseo/paseo/issues/4872: the slash +// autocomplete popover measures its anchor once per slash session, so a layout change +// under an open popover leaves it anchored to where the composer used to be. +test.describe("autocomplete popover anchoring", () => { + test("follows the composer when the sidebar closes while the popover is open", async ({ + page, + }) => { + await installListCommandsStub(page); + const session = await seedMockAgentWorkspace({ + repoPrefix: "repro-4872-anchor-a-", + title: "Repro 4872 anchor A", + }); + + try { + await openAgentRoute(page, session); + await expectComposerVisible(page, { timeout: 30_000 }); + const input = composerLocator(page); + await expect(input).toBeEditable({ timeout: 30_000 }); + await input.click(); + await page.keyboard.type("/"); + await expect(page.getByText("/help", { exact: true })).toBeVisible({ timeout: 30_000 }); + + console.log("A BEFORE", JSON.stringify(await boxes(page))); + await page.getByTestId("menu-button").first().click(); + await page.waitForTimeout(1200); + const after = await boxes(page); + console.log("A AFTER", JSON.stringify(after)); + + expect( + Math.abs((after.popover?.x ?? 0) - (after.composer?.x ?? 0)), + "popover stays anchored to the composer", + ).toBeLessThanOrEqual(4); + } finally { + await session.cleanup(); + } + }); + + test("stays on screen when the sidebar opens while the popover is open", async ({ page }) => { + await installListCommandsStub(page); + const session = await seedMockAgentWorkspace({ + repoPrefix: "repro-4872-anchor-b-", + title: "Repro 4872 anchor B", + }); + + try { + await openAgentRoute(page, session); + await expectComposerVisible(page, { timeout: 30_000 }); + + // Collapse the sidebar first, so the toggle under test expands it. + await page.getByTestId("menu-button").first().click(); + await page.waitForTimeout(1200); + + const input = composerLocator(page); + await expect(input).toBeEditable({ timeout: 30_000 }); + await input.click(); + await page.keyboard.type("/"); + await expect(page.getByText("/help", { exact: true })).toBeVisible({ timeout: 30_000 }); + console.log("B BEFORE", JSON.stringify(await boxes(page))); + + await page.getByTestId("menu-button").first().click(); + await page.waitForTimeout(1200); + const after = await boxes(page); + console.log("B AFTER", JSON.stringify(after), "viewport", JSON.stringify(page.viewportSize())); + + const popover = page.getByTestId("composer-autocomplete-popover").first(); + await expect(popover).toBeInViewport({ ratio: 0.9, timeout: 5_000 }); + } finally { + await session.cleanup(); + } + }); +});