test(dashboard): App.test.tsx is fully green — an incomplete mock was crashing NewTaskModal (#2846)
Closes #2829. `App.test.tsx` has been red since **2026-07-25**; #2833 took it 10 failures → 2, and this takes it to **0**. ## The cause, and it was not what I guessed `vi.mock("../../hooks/useViewportMode")` did not export `isShortViewport`, which `NewTaskModal` imports. An incomplete module mock **does not fail at the mock**. It throws inside whichever component imports the missing export, and the nearest `ErrorBoundary` swallows that into *"This section encountered an error"* — so the test failed on a **missing heading** with a DOM that looks perfectly healthy. Structurally the same presentation as the workflow-skeleton bug in #2833: the real failure sits two layers below what the assertion reports. Both remaining tests had this one cause. ## Found by probing, not theorising I had already been wrong twice on this file (`renders nothing`, then i18n), and my standing note said the modal was "plausibly FN-8620's FloatingWindow rework, **unverified**". That would have been a third wrong guess. Instead I dumped what was actually in the DOM at the assertion point: ``` headings: ["Fusion"] | dialogs: 0 newtask-ish: [..., "error-boundary error-boundary--modal"] ``` The `error-boundary--modal` class was the entire answer; reading its text gave the exact missing export. Same technique that cracked #2833 in one shot — the difference between the two halves of this investigation is that I stopped guessing. **App.test.tsx: 141 passed (141).** ## Also patched, and one deliberately not - **`Header.mobile-project-favorites.test.tsx`** mocks the same module with an inline factory and lacks the export. It is **latent, not broken** — its component does not import `isShortViewport` yet, and the day it does, the failure would be this same swallowed crash. One line. - **`FloatingWindow.touch-geometry.test.tsx`** greps as missing but is **not** — it uses `{ ...(await vi.importActual(...)) }`, so its surface is complete by construction. My grep-based classification was a false positive. That spread is the pattern that prevents this class outright, and it is the better fix **where it is available**. It is not available here: App.test.tsx's mock exists precisely so tests need no `window.matchMedia` in jsdom, and spreading the real module would reintroduce that dependency for every unstubbed export. So the scoped one-line stub is the right fix for this file, and the spread is the recommendation for new mocks. A guard asserting "a module mock exports everything the real module does" would close this class repo-wide. I did not build it — 34 files mock this module and only one was genuinely wrong, so the population does not yet justify another instrument, by the same measure-first rule that killed two other candidate gates this week. ## Verification App.test.tsx 141/141 · the two touched siblings 9/9 · `tsc` 0 · lint 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user