diff --git a/packages/dashboard/app/components/__tests__/App.test.tsx b/packages/dashboard/app/components/__tests__/App.test.tsx index 2b47232e1c..f0476183e8 100644 --- a/packages/dashboard/app/components/__tests__/App.test.tsx +++ b/packages/dashboard/app/components/__tests__/App.test.tsx @@ -639,6 +639,8 @@ vi.mock("../../hooks/useMobileKeyboard", () => ({ // Mock useViewportMode so tests can simulate mobile viewport without // depending on window.matchMedia in jsdom. const mockUseViewportMode = vi.fn(() => "desktop"); +/* `(max-height: 480px)` in production — independent of the width-driven mode. See the note below. */ +const mockIsShortViewport = vi.fn(() => false); vi.mock("../../hooks/useViewportMode", () => ({ MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", isTabletTouchViewport: (mode?: string) => mode === "tablet", @@ -646,6 +648,30 @@ vi.mock("../../hooks/useViewportMode", () => ({ getViewportMode: () => mockUseViewportMode(), isMobileViewport: () => mockUseViewportMode() === "mobile", isFullScreenSheetViewport: () => mockUseViewportMode() === "mobile", + /* + FNXC:TestViewportMock 2026-07-30-11:20: + An INCOMPLETE module mock does not fail where the export is missing — it throws inside whichever + component imports it, and the nearest ErrorBoundary swallows that into "This section encountered an + error". NewTaskModal adopted `isShortViewport`, this mock did not, and the test failed on a MISSING + HEADING with a healthy-looking DOM. + + FNXC:TestViewportMock 2026-07-30-19:50 (#2846 review — greptile P2, "viewport predicates are conflated"): + SHORT-VIEWPORT IS ITS OWN CONTROL, because in production it is its own MEDIA QUERY. + + The first version keyed it to `mode === "mobile"`, matching how the siblings above are stubbed. The + siblings are width predicates and the mode IS their answer; this one is not. `isShortViewport()` + reads `(max-height: 480px)` alone, while the mobile mode is the OR of width and height — so an + ordinary PORTRAIT PHONE (narrow, tall) is mobile and NOT short, and the mock claimed it was both. + + What that silently mis-tested: `FloatingWindow` suspends geometry PERSISTENCE on a short viewport, + and `PlanningModeModal` picks its compact interview layout and hides the session list from it. Every + mobile test here took those branches, so the ordinary phone case — the most common real viewport — + was never actually exercised, and a regression in the non-short mobile path would have passed. + + Defaults to FALSE rather than to the mode: a test that means "short" now has to say so, which is the + only spelling that can distinguish the two. + */ + isShortViewport: () => mockIsShortViewport(), })); // Mock isIOS so FN-3290 keyboard-open behavior is testable in jsdom @@ -824,6 +850,9 @@ beforeEach(() => { }); mockUseViewportMode.mockReset(); mockUseViewportMode.mockReturnValue("desktop"); + /* Reset alongside the mode: it is a SEPARATE predicate, so a suite that sets it must not leak. */ + mockIsShortViewport.mockReset(); + mockIsShortViewport.mockReturnValue(false); mockAgentStats.todoTaskCount = 0; mockAgentStats.idleNonEphemeralCount = 1; }); diff --git a/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx b/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx index 5f88738be6..5fb9e28aa6 100644 --- a/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx +++ b/packages/dashboard/app/components/__tests__/Header.mobile-project-favorites.test.tsx @@ -14,6 +14,10 @@ vi.mock("../../api", async (importOriginal) => ({ vi.mock("../../hooks/useViewportMode", () => ({ isTabletTouchViewport: (mode?: string) => mode === "tablet", useViewportMode: () => "mobile", + /* FNXC:TestViewportMock 2026-07-30-11:30: keep this mock's surface complete. A missing export does + not fail here — it throws inside the importing component and the nearest ErrorBoundary turns it + into a missing element, which is how App.test.tsx stayed red for five days. */ + isShortViewport: () => false, })); function makeProject(id: string, name: string): ProjectInfo {