mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
* fix(app): let Escape close an open popover before it interrupts the agent The agent.interrupt shortcut listens on the window in the capture phase, so it sees Escape before anything else. Registered overlays get first refusal through dispatchTopWebOverlayKeyDown, but plugins cannot register, and draw their floating panels with the Popover API instead. With such a panel open and the composer focused while an agent runs, Escape closed the panel and interrupted the agent in the same press. While a popover is open, Escape is left to it and no app shortcut runs. A second Escape, with nothing open, interrupts as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com> * test(app): cover Escape with a popover open through the shortcut path The first test only checked hasOpenDomPopover and would still pass with the guard removed from useKeyboardShortcuts. The check is now keyBelongsToDomPopover, called by the hook, and a browser test drives it the way the app does: a window capture listener for shortcuts, a popover closed by its own document Escape listener, focus in a textarea. The first Escape closes the popover with no interrupt; the second interrupts. Both new tests fail with the guard removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com> * test(app): exercise plugin Escape through the real composer Keep the contributor fix and replace the substitute shortcut-listener test with an installed-plugin browser regression. The first Escape must close the preview without cancelling the turn; the second must cancel it. Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com> * test(app): check dismissed model pickers leave the viewport --------- Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
import { expect, test, type Page } from "../support/fixtures";
|
|
import { expectComposerVisible } from "../support/helpers/composer";
|
|
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
|
|
const MOBILE_VIEWPORT = { width: 390, height: 844 };
|
|
|
|
async function openMockAgentAtMobileBreakpoint(page: Page) {
|
|
await page.setViewportSize(MOBILE_VIEWPORT);
|
|
const session = await seedMockAgentWorkspace({
|
|
repoPrefix: "bottom-sheet-reopen-",
|
|
title: "Bottom sheet reopen e2e",
|
|
initialPrompt: "Prepare a bottom sheet reopen test agent.",
|
|
model: "ten-second-stream",
|
|
});
|
|
await openAgentRoute(page, session);
|
|
await expect(page.getByTestId("workspace-tab-switcher-trigger")).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await expectComposerVisible(page);
|
|
await expect(page.getByRole("button", { name: /Select model/ })).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
return session;
|
|
}
|
|
|
|
function bottomSheetBackdrop(page: Page) {
|
|
return page.getByRole("button", { name: "Bottom sheet backdrop" }).first();
|
|
}
|
|
|
|
function bottomSheetHandle(page: Page) {
|
|
return page.getByRole("slider", { name: "Bottom sheet handle" }).first();
|
|
}
|
|
|
|
async function expectBottomSheetOpen(page: Page) {
|
|
await expect(bottomSheetBackdrop(page)).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
|
|
async function closeBottomSheetWithBackdrop(page: Page) {
|
|
const backdrop = bottomSheetBackdrop(page);
|
|
const handle = bottomSheetHandle(page);
|
|
// Tapping the backdrop is the close path under test, but on a loaded CI runner
|
|
// the model-selector sheet re-renders as its model list settles and Gorhom
|
|
// drops backdrop presses during that churn — so a tap (even retried) can fail
|
|
// to dismiss. Tap the backdrop first; if it survives, drag the handle down,
|
|
// which drives Gorhom's pan-to-close directly and is unaffected by the churn.
|
|
// The post-close guard below still protects the regression this test exists
|
|
// for: a sheet that dismisses, then re-presents.
|
|
await expect(async () => {
|
|
if (!(await backdrop.isVisible())) {
|
|
return;
|
|
}
|
|
const box = await backdrop.boundingBox();
|
|
if (box) {
|
|
await page.mouse.click(box.x + box.width / 2, box.y + 24);
|
|
}
|
|
await page.waitForTimeout(150);
|
|
if (await backdrop.isVisible()) {
|
|
const handleBox = await handle.boundingBox();
|
|
if (handleBox) {
|
|
const startX = handleBox.x + handleBox.width / 2;
|
|
const startY = handleBox.y + handleBox.height / 2;
|
|
await page.mouse.move(startX, startY);
|
|
await page.mouse.down();
|
|
await page.mouse.move(startX, startY + 400, { steps: 8 });
|
|
await page.mouse.up();
|
|
}
|
|
}
|
|
await expect(backdrop).not.toBeVisible({ timeout: 1_000 });
|
|
}).toPass({ timeout: 15_000 });
|
|
// Guard against the regression where the sheet starts dismissing, then re-presents.
|
|
await page.waitForTimeout(500);
|
|
await expect(backdrop).not.toBeVisible({ timeout: 1_000 });
|
|
}
|
|
|
|
async function openTabSwitcher(page: Page) {
|
|
const trigger = page.getByRole("button", { name: /Switch tabs/ });
|
|
await trigger.click();
|
|
await expectBottomSheetOpen(page);
|
|
}
|
|
|
|
async function openModelSelector(page: Page) {
|
|
await page.getByRole("button", { name: /Select model/ }).click();
|
|
await expectBottomSheetOpen(page);
|
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
|
|
async function dismissStackedModelPickerWithBackdrop(page: Page) {
|
|
await page
|
|
.getByTestId("agent-controls-settings-list")
|
|
.getByRole("button", { name: /Select model/ })
|
|
.click();
|
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).toBeInViewport();
|
|
await page.mouse.click(MOBILE_VIEWPORT.width / 2, 24);
|
|
// In CI, Gorhom retains the dismissed sheet below the viewport.
|
|
// Dismissal is complete when it leaves the displayed surface.
|
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).not.toBeInViewport();
|
|
await expect(page.getByTestId("agent-controls-model-sheet")).toBeVisible();
|
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible();
|
|
}
|
|
|
|
async function openAndCloseTabSwitcherTwice(page: Page) {
|
|
await openTabSwitcher(page);
|
|
await closeBottomSheetWithBackdrop(page);
|
|
await openTabSwitcher(page);
|
|
await closeBottomSheetWithBackdrop(page);
|
|
}
|
|
|
|
async function openAndCloseModelSelectorTwice(page: Page) {
|
|
await openModelSelector(page);
|
|
await closeBottomSheetWithBackdrop(page);
|
|
await openModelSelector(page);
|
|
await closeBottomSheetWithBackdrop(page);
|
|
}
|
|
|
|
test.describe("mobile bottom sheet reopen", () => {
|
|
test("sheets reopen and model search returns to configuration", async ({ page }) => {
|
|
const session = await openMockAgentAtMobileBreakpoint(page);
|
|
try {
|
|
await test.step("tab switcher opens, closes, and reopens", async () => {
|
|
await openAndCloseTabSwitcherTwice(page);
|
|
});
|
|
|
|
await test.step("model selector opens, closes, and reopens", async () => {
|
|
await openAndCloseModelSelectorTwice(page);
|
|
});
|
|
|
|
await test.step("model search returns to configuration", async () => {
|
|
await openModelSelector(page);
|
|
const sheet = page.getByTestId("agent-controls-model-sheet");
|
|
|
|
await dismissStackedModelPickerWithBackdrop(page);
|
|
|
|
await page.getByTestId("model-search-all-input").click();
|
|
const model = page.getByRole("button", { name: /^Ten second stream/ });
|
|
await expect(model).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
|
|
await model.click();
|
|
|
|
await expect(sheet).toBeVisible();
|
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible();
|
|
await expect(page.getByTestId("agent-controls-model")).toContainText("Ten second stream");
|
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).not.toBeInViewport();
|
|
});
|
|
} finally {
|
|
await session.cleanup();
|
|
}
|
|
});
|
|
});
|