From 95ca1f126758e2ca47e5cc2c88354a4e28b26f75 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 20 Jul 2026 12:10:34 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20keep=20analytics=20si?= =?UTF-8?q?debar=20controllable=20on=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/Analytics/AnalyticsDashboard.tsx | 4 +- .../stories/App.phoneViewports.stories.tsx | 44 ++++++++++++++++++- tests/ui/layout/analyticsHeader.test.ts | 32 ++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/browser/features/Analytics/AnalyticsDashboard.tsx b/src/browser/features/Analytics/AnalyticsDashboard.tsx index 8687218fd1..9af4a3bbc8 100644 --- a/src/browser/features/Analytics/AnalyticsDashboard.tsx +++ b/src/browser/features/Analytics/AnalyticsDashboard.tsx @@ -209,7 +209,9 @@ export function AnalyticsDashboard(props: AnalyticsDashboardProps) { onClick={props.onToggleLeftSidebarCollapsed} title="Open sidebar" aria-label="Open sidebar" - className="text-muted hover:text-foreground hidden h-6 w-6 md:inline-flex" + // The left sidebar is fully off-canvas on narrow viewports, so this route must + // expose the shared mobile menu control instead of hiding its only opener. + className="mobile-menu-btn text-muted hover:text-foreground hidden h-6 w-6 md:inline-flex" > diff --git a/src/browser/stories/App.phoneViewports.stories.tsx b/src/browser/stories/App.phoneViewports.stories.tsx index 3f8d66643d..c01e857a09 100644 --- a/src/browser/stories/App.phoneViewports.stories.tsx +++ b/src/browser/stories/App.phoneViewports.stories.tsx @@ -5,7 +5,7 @@ * Pixel snapshots both light and dark themes at the phone viewport. */ -import { within, waitFor } from "@storybook/test"; +import { userEvent, within, waitFor } from "@storybook/test"; import type { ComponentType } from "react"; import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; @@ -138,6 +138,48 @@ export const IPhone16e: AppStory = { }, }; +export const IPhone16eAnalyticsSidebarControl: AppStory = { + globals: { + viewport: { value: "mobile1", isRotated: false }, + }, + render: () => ( + { + const client = setupSimpleChatStory({ + workspaceId: "ws-iphone-analytics", + workspaceName: "analytics-mobile", + projectName: "mux", + messages: [...MESSAGES], + }); + window.localStorage.setItem(LEFT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(false)); + return client; + }} + /> + ), + decorators: [IPhone16eDecorator], + parameters: { + ...appMeta.parameters, + pixel: { + matrix: { themes: ["dark", "light"], viewports: ["phone"] }, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await stabilizePhoneViewportStory(canvasElement); + + await userEvent.click(await canvas.findByTestId("analytics-button")); + await canvas.findByTestId("analytics-header"); + await userEvent.click(canvas.getByRole("button", { name: "Collapse sidebar" })); + + const openSidebarButton = await canvas.findByRole("button", { name: "Open sidebar" }); + if (!openSidebarButton.classList.contains("mobile-menu-btn")) { + throw new Error("Analytics sidebar opener is not enabled for the phone viewport"); + } + + blurActiveElement(); + }, +}; + export const IPhone17ProMax: AppStory = { render: () => ( { } }, 60_000); + test("narrow browser mode can reopen the off-canvas left sidebar", async () => { + const app = await createAppHarness({ branchPrefix: "analytics-sidebar-mobile" }); + + try { + const header = await openAnalyticsAndGetHeader(app.view.container); + const collapseSidebarButton = app.view.container.querySelector( + 'button[aria-label="Collapse sidebar"]' + ) as HTMLButtonElement | null; + expect(collapseSidebarButton).not.toBeNull(); + fireEvent.click(collapseSidebarButton!); + + const openSidebarButton = await waitFor(() => { + const button = header.querySelector( + 'button[aria-label="Open sidebar"]' + ) as HTMLButtonElement | null; + if (!button) { + throw new Error("Analytics sidebar opener not found"); + } + return button; + }); + expect(openSidebarButton.classList.contains("mobile-menu-btn")).toBe(true); + + fireEvent.click(openSidebarButton); + + await waitFor(() => { + expect(document.documentElement.dataset.leftSidebarCollapsed).toBe("false"); + }); + } finally { + await app.dispose(); + } + }, 60_000); + test("desktop linux mode: header has h-9, titlebar-drag, and titlebar-safe-right", async () => { const app = await createAppHarness({ branchPrefix: "analytics-hdr-linux", From 1f400f38de0adaf92824671bb9e9972162873dc5 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 20 Jul 2026 12:20:52 -0500 Subject: [PATCH 2/3] Address sidebar story review --- src/browser/stories/App.phoneViewports.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/stories/App.phoneViewports.stories.tsx b/src/browser/stories/App.phoneViewports.stories.tsx index c01e857a09..5192f1a192 100644 --- a/src/browser/stories/App.phoneViewports.stories.tsx +++ b/src/browser/stories/App.phoneViewports.stories.tsx @@ -10,6 +10,7 @@ import type { ComponentType } from "react"; import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; +import { updatePersistedState } from "@/browser/hooks/usePersistedState"; import { LEFT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage"; import { appMeta, AppWithMocks, type AppStory } from "./meta.js"; @@ -151,7 +152,7 @@ export const IPhone16eAnalyticsSidebarControl: AppStory = { projectName: "mux", messages: [...MESSAGES], }); - window.localStorage.setItem(LEFT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(false)); + updatePersistedState(LEFT_SIDEBAR_COLLAPSED_KEY, false); return client; }} /> From 2433fc054f0c11f4495f60427d720eed1481247a Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 20 Jul 2026 12:44:43 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20tests:=20remove=20live=20mod?= =?UTF-8?q?el=20refresh=20flake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ui/helpers.ts | 37 --------------------------------- tests/ui/review/refresh.test.ts | 36 +++++++------------------------- 2 files changed, 8 insertions(+), 65 deletions(-) diff --git a/tests/ui/helpers.ts b/tests/ui/helpers.ts index d8f6c1a011..bae343a783 100644 --- a/tests/ui/helpers.ts +++ b/tests/ui/helpers.ts @@ -11,43 +11,6 @@ import type { RenderedApp } from "./renderReviewPanel"; import { workspaceStore } from "@/browser/stores/WorkspaceStore"; import { useGitStatusStoreRaw } from "@/browser/stores/GitStatusStore"; -// ═══════════════════════════════════════════════════════════════════════════════ -// TYPES -// ═══════════════════════════════════════════════════════════════════════════════ - -export type EventCollector = { getEvents(): unknown[] }; - -type ToolCallEndEvent = { type: "tool-call-end"; toolName: string }; - -// ═══════════════════════════════════════════════════════════════════════════════ -// EVENT HELPERS -// ═══════════════════════════════════════════════════════════════════════════════ - -function isToolCallEndEvent(event: unknown): event is ToolCallEndEvent { - if (typeof event !== "object" || event === null) return false; - const record = event as { type?: unknown; toolName?: unknown }; - return record.type === "tool-call-end" && typeof record.toolName === "string"; -} - -/** - * Wait for a tool-call-end event with the specified tool name. - */ -export async function waitForToolCallEnd( - collector: EventCollector, - toolName: string, - timeoutMs: number = 10_000 -): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const match = collector - .getEvents() - .find((event) => isToolCallEndEvent(event) && event.toolName === toolName); - if (match) return; - await new Promise((r) => setTimeout(r, 50)); - } - throw new Error(`Timed out waiting for tool-call-end: ${toolName}`); -} - // ═══════════════════════════════════════════════════════════════════════════════ // REFRESH BUTTON HELPERS // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/tests/ui/review/refresh.test.ts b/tests/ui/review/refresh.test.ts index 52fe18ac37..ba4383f6e2 100644 --- a/tests/ui/review/refresh.test.ts +++ b/tests/ui/review/refresh.test.ts @@ -1,7 +1,7 @@ import "../dom"; import { fireEvent, waitFor } from "@testing-library/react"; -import { shouldRunIntegrationTests, validateApiKeys } from "../../testUtils"; +import { shouldRunIntegrationTests } from "../../testUtils"; import { STORAGE_KEYS } from "@/constants/workspaceDefaults"; import { getReviewsKey } from "@/common/constants/storage"; import { @@ -10,15 +10,11 @@ import { createSharedRepo, withSharedWorkspace, } from "../../ipc/sendMessageTestHelpers"; -import { HAIKU_MODEL, sendMessageWithModel } from "../../ipc/helpers"; -import type { ToolPolicy } from "../../../src/common/utils/tools/toolPolicy"; - import { installDom } from "../dom"; import { renderReviewPanel, type RenderedApp } from "../renderReviewPanel"; import { cleanupView, setupWorkspaceView, - waitForToolCallEnd, waitForRefreshButtonIdle, assertRefreshButtonHasLastRefreshInfo, simulateFileModifyingToolEnd, @@ -31,8 +27,6 @@ configureTestRetries(2); const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; -validateApiKeys(["ANTHROPIC_API_KEY"]); - /** * Helper to set up the full App UI and navigate to the Review tab. * Returns the refresh button for assertions. @@ -425,10 +419,10 @@ describeIntegration("ReviewPanel simulated tool refresh (UI + ORPC, no LLM)", () }); // ═══════════════════════════════════════════════════════════════════════════════ -// AUTO REFRESH TEST (slow, requires LLM) +// AUTO REFRESH TEST // ═══════════════════════════════════════════════════════════════════════════════ -describeIntegration("ReviewPanel auto refresh (UI + ORPC + live LLM)", () => { +describeIntegration("ReviewPanel auto refresh (UI + ORPC)", () => { beforeAll(async () => { await createSharedRepo(); }); @@ -438,7 +432,7 @@ describeIntegration("ReviewPanel auto refresh (UI + ORPC + live LLM)", () => { }); test("tool-call-end triggers scheduled refresh", async () => { - await withSharedWorkspace("anthropic", async ({ env, workspaceId, collector, metadata }) => { + await withSharedWorkspace("anthropic", async ({ env, workspaceId, metadata }) => { const cleanupDom = installDom(); const view = renderReviewPanelForRefreshTests({ @@ -465,24 +459,10 @@ describeIntegration("ReviewPanel auto refresh (UI + ORPC + live LLM)", () => { // Without a scheduled refresh, the UI should not pick this up yet. expect(view.queryByText(new RegExp(AUTO_MARKER))).toBeNull(); - // Trigger a tool-call-end event via bash. - const FORCE_BASH: ToolPolicy = [{ regex_match: "bash", action: "require" }]; - - const autoRes = await sendMessageWithModel( - env, - workspaceId, - 'Use bash to run: echo ping. Set display_name="ping" and timeout_secs=30. Do not modify files.', - HAIKU_MODEL, - { - agentId: "exec", - thinkingLevel: "off", - toolPolicy: FORCE_BASH, - } - ); - expect(autoRes.success).toBe(true); - - await collector.waitForEvent("stream-end", 30_000); - await waitForToolCallEnd(collector, "bash"); + // Exercise the same WorkspaceStore event emitted after file-modifying tools complete. + // A live model made this behavioral test depend on whether the provider chose/called bash, + // which repeatedly flaked in CI before the event under test was ever emitted. + simulateFileModifyingToolEnd(workspaceId); // Verify the workspace actually changed const statusRes = await env.orpc.workspace.executeBash({