Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 43 additions & 31 deletions packages/app/test/ui-smoke/launcher-interaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const OUT_DIR = path.join(
process.cwd(),
".github",
"issue-evidence",
"9144-default-launcher-dock",
"9144-launcher-page-tiles",
);

async function screenshot(page: Page, name: string): Promise<void> {
Expand Down Expand Up @@ -49,16 +49,19 @@ async function tileIds(scope: Locator): Promise<string[]> {
);
}

async function advanceLauncherPage(
async function swipeLauncherPage(
page: Page,
direction: "next" | "prev",
): Promise<"pointer-swipe" | "edge-button"> {
const pageWindow = page.getByTestId("launcher-page-window");
const secondPage = page.getByTestId("launcher-page-1");
const targetPage = page.getByTestId(
direction === "next" ? "launcher-page-1" : "launcher-page-0",
);
const box = await pageWindow.boundingBox();
if (!box) throw new Error("launcher page window is not laid out");
const y = box.y + box.height * 0.52;
const startX = box.x + box.width * 0.82;
const endX = box.x + box.width * 0.16;
const startX = box.x + box.width * (direction === "next" ? 0.82 : 0.16);
const endX = box.x + box.width * (direction === "next" ? 0.16 : 0.82);
const pointer = {
bubbles: true,
cancelable: true,
Expand Down Expand Up @@ -88,35 +91,38 @@ async function advanceLauncherPage(
clientX: endX,
clientY: y + 2,
});
const swiped = await secondPage
const swiped = await targetPage
.evaluate((node) => node.getAttribute("aria-hidden") === "false")
.catch(() => false);
if (swiped) return "pointer-swipe";

const next = page.getByTestId("launcher-pager-edge-next");
if ((await next.count()) === 0) {
const edgeButton = page.getByTestId(`launcher-pager-edge-${direction}`);
if ((await edgeButton.count()) === 0) {
throw new Error(
"launcher pointer swipe did not advance and no next pager button rendered",
`launcher pointer swipe did not move to the ${direction} page and no ${direction} pager button rendered`,
);
}
await next.click();
await edgeButton.click();
return "edge-button";
}

/**
* Interaction-level coverage for the iOS-like view catalog (Launcher, #8796).
*
* Unlike builtin-views-visual.spec (which only asserts each view boots without
* crashing), this drives the catalog's actual controls — seeded default dock,
* swipe paging, and tap-to-launch — against a live app boot. Run with
* E2E_RECORD=1 to capture a video walkthrough.
* crashing), this drives the catalog's actual controls against a live app
* boot. The launcher has NO dock / featured-views surface (#11174): every view
* is an ordinary tile on the swipeable pages, with the curated "Apps" page
* (page 0) leading with Chat and Settings. Covered here: the no-dock contract,
* curated page-0 ordering, swipe paging, and tap-to-launch of the Chat tile.
* Run with E2E_RECORD=1 to capture a video walkthrough.
*/
test.describe("launcher catalog interactions", () => {
for (const viewport of [
{ name: "desktop", size: { width: 1440, height: 1000 } },
{ name: "mobile", size: { width: 390, height: 844 } },
] as const) {
test(`default dock, swipe paging, and dock Chat launch on ${viewport.name}`, async ({
test(`no dock, page tiles, swipe paging, and Chat tile launch on ${viewport.name}`, async ({
page,
}, testInfo) => {
const consoleLines: string[] = [];
Expand All @@ -142,15 +148,14 @@ test.describe("launcher catalog interactions", () => {
await expect(page.getByTestId("launcher")).toBeVisible({
timeout: 60_000,
});
const dock = page.getByTestId("launcher-dock");
const firstPage = page.getByTestId("launcher-page-0");
await expect(dock).toBeVisible();
await expect(dock.getByTestId("launcher-tile-chat")).toBeVisible();
await expect(dock.getByTestId("launcher-tile-settings")).toBeVisible();
await expect(firstPage.getByTestId("launcher-tile-chat")).toHaveCount(0);
await expect(firstPage.getByTestId("launcher-tile-settings")).toHaveCount(
0,
);
// The featured-views dock was removed (#11174): no dock element exists,
// and Chat/Settings are ordinary page tiles at the head of page 0.
await expect(page.getByTestId("launcher-dock")).toHaveCount(0);
await expect(firstPage.getByTestId("launcher-tile-chat")).toBeVisible();
await expect(
firstPage.getByTestId("launcher-tile-settings"),
).toBeVisible();
await expect(
firstPage.locator('[data-testid^="launcher-tile-"]').first(),
).toBeVisible();
Expand All @@ -159,41 +164,48 @@ test.describe("launcher catalog interactions", () => {
await expect(page.getByRole("button", { name: "Done" })).toHaveCount(0);

await page.waitForTimeout(300);
await screenshot(page, `${viewport.name}-launcher-default-dock`);
const dockTileIds = await tileIds(dock);
await screenshot(page, `${viewport.name}-launcher-page-tiles`);
const firstPageTileIds = await tileIds(firstPage);

const secondPage = page.getByTestId("launcher-page-1");
let pageAdvanceMethod: "pointer-swipe" | "edge-button" | "single-page" =
"single-page";
if ((await secondPage.count()) > 0) {
pageAdvanceMethod = await advanceLauncherPage(page);
pageAdvanceMethod = await swipeLauncherPage(page, "next");
await expect(secondPage).toHaveAttribute("aria-hidden", "false");
await page.waitForTimeout(300);
await screenshot(page, `${viewport.name}-launcher-after-swipe`);
}

await dock.getByTestId("launcher-tile-chat").locator("button").click();
// Chat launches from its ordinary page tile. Return to page 0 first if
// the paging step above advanced (page-1 tiles are inert while inactive).
if (pageAdvanceMethod !== "single-page") {
await swipeLauncherPage(page, "prev");
await expect(firstPage).toHaveAttribute("aria-hidden", "false");
}
await firstPage
.getByTestId("launcher-tile-chat")
.locator("button")
.click();
await expect
.poll(() => new URL(page.url()).hash + new URL(page.url()).pathname)
.toContain("/chat");
await expect(page.getByTestId("chat-composer-textarea")).toBeVisible();
await page.waitForTimeout(300);
await screenshot(page, `${viewport.name}-dock-chat-launched`);
await screenshot(page, `${viewport.name}-chat-tile-launched`);

const evidence = {
viewport: viewport.name,
dockTiles: dockTileIds,
firstPageTiles: firstPageTileIds,
pageAdvanceMethod,
finalUrl: page.url(),
pageErrors,
httpErrors,
consoleLines,
};
expect(evidence.dockTiles.slice(0, 2)).toEqual(["chat", "settings"]);
expect(evidence.firstPageTiles).not.toContain("chat");
expect(evidence.firstPageTiles).not.toContain("settings");
// Curated "Apps" page order leads with Chat then Settings
// (launcher-curation.ts APPS_PAGE_ORDER) — as page tiles, not a dock.
expect(evidence.firstPageTiles.slice(0, 2)).toEqual(["chat", "settings"]);
expect(pageErrors, "no uncaught page errors").toEqual([]);
expect(httpErrors, "no HTTP error responses").toEqual([]);

Expand Down
24 changes: 24 additions & 0 deletions packages/ui/src/components/shell/ContinuousChatOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,30 @@ describe("ContinuousChatOverlay", () => {
expect(sheet.getAttribute("data-variant")).toBe("closed");
});

it("lets the desktop notification panel own Escape — the chat only collapses once the panel is gone", () => {
render(<ContinuousChatOverlay controller={makeController()} />);
const sheet = screen.getByTestId("chat-sheet");
fireEvent.focus(screen.getByLabelText("message"));
expect(sheet.getAttribute("data-variant")).toBe("open");

// The desktop anchored notification panel carries role="dialog" but NO
// data-state="open" (it is not a Radix dialog), so it wouldn't match the
// dialog guard — Escape must close IT alone, not also collapse the chat.
const panel = document.createElement("div");
panel.setAttribute("role", "dialog");
panel.setAttribute("data-testid", "notification-panel");
document.body.appendChild(panel);
try {
fireEvent.keyDown(document.body, { key: "Escape" });
expect(sheet.getAttribute("data-variant")).toBe("open");
} finally {
panel.remove();
}
// Panel gone: Escape collapses the chat as before.
fireEvent.keyDown(document.body, { key: "Escape" });
expect(sheet.getAttribute("data-variant")).toBe("closed");
});

it("Escape closes an in-progress message edit without collapsing the whole sheet (#9148)", () => {
render(
<ContinuousChatOverlay
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/shell/ContinuousChatOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3346,7 +3346,9 @@ export function ContinuousChatOverlay({
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
// An open Radix dialog (data-state="open" — e.g. the command palette)
// or the notification pull-down sheet (mounts only while open) sits
// or a notification surface (the mobile pull-down sheet or the desktop
// anchored panel — both mount only while open; the panel carries
// role="dialog" with NO data-state="open") sits
// above the chat: let ITS Escape handling win — collapsing here too
// closed both at once (e.g. an invisible palette + the chat). Scoped
// to exactly these; broad role="dialog" would match always-mounted
Expand All @@ -3359,7 +3361,7 @@ export function ContinuousChatOverlay({
// NOT also collapse the whole sheet + discard the in-progress edit.
if (
document.querySelector(
'[role="dialog"][data-state="open"], [data-testid="notification-sheet"], [data-testid="transcript-viewer"], [data-testid="thread-line-edit-input"]',
'[role="dialog"][data-state="open"], [data-testid="notification-sheet"], [data-testid="notification-panel"], [data-testid="transcript-viewer"], [data-testid="thread-line-edit-input"]',
)
) {
return;
Expand Down
Loading