From 26fa4790038ee4793cdb155d0482fdd2be142a78 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 7 Jun 2026 17:49:34 -0700 Subject: [PATCH] FN-6005: add mobile auto-merge blank-screen regression coverage Add broader regression coverage for the mobile auto-merge blank-screen dashboard issue. - expand the mobile integration test harness to cover viewport, keyboard, and mobile nav shell behavior - add regression cases for empty in-review states, detail panels, update rollbacks, and app-shell round trips - verify sibling render failures surface the page error boundary fallback instead of leaving a blank screen Files changed: packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile-integration.test.tsx | 220 ++++++++++++++++++++- 1 file changed, 217 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6005 Fusion-Task-Lineage: 7cbba538-343b-4540-910f-08ba6ce7d183 --- ...e-toggle-blank.mobile-integration.test.tsx | 220 +++++++++++++++++- 1 file changed, 217 insertions(+), 3 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile-integration.test.tsx b/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile-integration.test.tsx index ef2aa46dd1..bf0fef3546 100644 --- a/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile-integration.test.tsx +++ b/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile-integration.test.tsx @@ -5,9 +5,11 @@ import type { Settings, Task } from "@fusion/core"; import { Board } from "../Board"; import { PageErrorBoundary } from "../ErrorBoundary"; import { TaskReviewTab } from "../TaskReviewTab"; +import { MobileNavBar } from "../MobileNavBar"; import { RetryWarningProvider } from "../../context/RetryWarningContext"; import { useAppSettings } from "../../hooks/useAppSettings"; -import { MOBILE_MEDIA_QUERY } from "../../hooks/useViewportMode"; +import { useMobileKeyboard, _resetInitialViewportHeight } from "../../hooks/useMobileKeyboard"; +import { MOBILE_MEDIA_QUERY, useViewportMode } from "../../hooks/useViewportMode"; import { fetchConfig, fetchSettings, fetchTaskReview, updateSettings } from "../../api"; const defaultSettings: Settings = { @@ -160,7 +162,23 @@ function createTask(id: string, column: Task["column"], overrides: Partial } as Task; } -function SettingsBoardHarness({ tasks, openTaskOnMountId }: { tasks: Task[]; openTaskOnMountId?: string }) { +function ThrowOnAutoMergeOff({ autoMerge }: { autoMerge: boolean }) { + if (autoMerge === false) { + throw new Error("Auto-merge render failed"); + } + + return null; +} + +function SettingsBoardHarness({ + tasks, + openTaskOnMountId, + includeThrowProbe = false, +}: { + tasks: Task[]; + openTaskOnMountId?: string; + includeThrowProbe?: boolean; +}) { const { autoMerge, toggleAutoMerge, maxConcurrent } = useAppSettings("proj_123"); const [selectedTask, setSelectedTask] = useState(null); const handleOpenDetail = (task: Task) => setSelectedTask(task); @@ -197,11 +215,73 @@ function SettingsBoardHarness({ tasks, openTaskOnMountId }: { tasks: Task[]; ope ) : null} + {includeThrowProbe ? : null} ); } +function AppShellMobileHarness({ tasks }: { tasks: Task[] }) { + const viewportMode = useViewportMode(); + const isMobile = viewportMode === "mobile"; + const { keyboardOpen } = useMobileKeyboard({ enabled: isMobile }); + const { autoMerge, toggleAutoMerge, maxConcurrent } = useAppSettings("proj_123"); + + return ( + +
+ + ({} as Task))} + onOpenDetail={vi.fn()} + addToast={vi.fn()} + onQuickCreate={vi.fn(async () => undefined)} + onNewTask={vi.fn()} + autoMerge={autoMerge} + onToggleAutoMerge={toggleAutoMerge} + globalPaused={false} + prAuthAvailable={true} + /> + +
+ {isMobile && !keyboardOpen ? ( + + ) : null} +
+ ); +} + function expectBoardVisible(taskTitles: string[] = []) { expect(document.querySelector("main.board")).not.toBeNull(); expect(screen.getByText("In Review")).toBeInTheDocument(); @@ -222,18 +302,53 @@ function createInReviewAndWorktreeTasks() { ]; } +function installMobileDeviceEnvironment() { + Object.defineProperty(window.navigator, "maxTouchPoints", { + configurable: true, + value: 5, + }); +} + +function renderAppShellHarness({ + width, + height = 812, + tasks, + autoMerge = false, +}: { + width: number; + height?: number; + tasks: Task[]; + autoMerge?: Settings["autoMerge"]; +}) { + const viewportSpy = mockViewport(width, height); + const visualViewport = createVisualViewport(); + Object.defineProperty(window, "visualViewport", { + configurable: true, + value: visualViewport, + }); + installMobileDeviceEnvironment(); + installAnimationFrame(); + mockSettings = { ...defaultSettings, autoMerge }; + + render(); + + return { viewportSpy, visualViewport }; +} + function renderBoardHarness({ width, height = 812, tasks, autoMerge = false, openTaskOnMountId, + includeThrowProbe = false, }: { width: number; height?: number; tasks: Task[]; autoMerge?: Settings["autoMerge"]; openTaskOnMountId?: string; + includeThrowProbe?: boolean; }) { const viewportSpy = mockViewport(width, height); const visualViewport = createVisualViewport(); @@ -244,7 +359,13 @@ function renderBoardHarness({ installAnimationFrame(); mockSettings = { ...defaultSettings, autoMerge }; - render(); + render( + , + ); return { viewportSpy, visualViewport }; } @@ -253,10 +374,12 @@ describe("auto-merge toggle mobile integration regression", () => { beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); + _resetInitialViewportHeight(); mockSettings = { ...defaultSettings }; }); afterEach(() => { + _resetInitialViewportHeight(); vi.useRealTimers(); vi.unstubAllGlobals(); }); @@ -365,6 +488,97 @@ describe("auto-merge toggle mobile integration regression", () => { viewportSpy.mockRestore(); }); + it("shows the page error boundary fallback instead of a blank page when a sibling render throws after auto-merge toggles off", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const { viewportSpy, visualViewport } = renderBoardHarness({ + width: 375, + tasks: createInReviewAndWorktreeTasks(), + autoMerge: true, + includeThrowProbe: true, + }); + + await act(async () => { + await Promise.resolve(); + }); + act(() => { + vi.advanceTimersByTime(1); + }); + + const toggle = screen.getByRole("checkbox", { name: "Auto-merge" }); + expect(toggle).toBeChecked(); + expectBoardVisible(["FN-5972", "Worktree child task"]); + + await act(async () => { + fireEvent.click(toggle); + await Promise.resolve(); + }); + act(() => { + visualViewport.dispatchResize(); + vi.advanceTimersByTime(1); + }); + + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument(); + expect(document.querySelector("main.board")).toBeNull(); + + consoleErrorSpy.mockRestore(); + viewportSpy.mockRestore(); + }); + + it("keeps the App-level mobile shell visible while round-tripping auto-merge", async () => { + const { viewportSpy, visualViewport } = renderAppShellHarness({ + width: 375, + tasks: createInReviewAndWorktreeTasks(), + autoMerge: false, + }); + + await act(async () => { + await Promise.resolve(); + }); + act(() => { + vi.advanceTimersByTime(1); + }); + + const toggle = screen.getByRole("checkbox", { name: "Auto-merge" }); + expect(toggle).not.toBeChecked(); + expectBoardVisible(["FN-5972", "Worktree child task"]); + expect(document.querySelector(".project-content.project-content--with-mobile-nav")).not.toBeNull(); + expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeInTheDocument(); + expect(screen.queryByText("Something went wrong")).toBeNull(); + + await act(async () => { + fireEvent.click(toggle); + await Promise.resolve(); + }); + act(() => { + visualViewport.dispatchResize(); + vi.advanceTimersByTime(1); + }); + + expect(toggle).toBeChecked(); + expectBoardVisible(["FN-5972", "Worktree child task"]); + expect(document.querySelector(".project-content.project-content--with-mobile-nav")).not.toBeNull(); + expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeInTheDocument(); + expect(screen.queryByText("Something went wrong")).toBeNull(); + + await act(async () => { + fireEvent.click(toggle); + await Promise.resolve(); + }); + act(() => { + visualViewport.dispatchResize(); + vi.advanceTimersByTime(1); + }); + + expect(toggle).not.toBeChecked(); + expectBoardVisible(["FN-5972", "Worktree child task"]); + expect(document.querySelector(".project-content.project-content--with-mobile-nav")).not.toBeNull(); + expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeInTheDocument(); + expect(screen.queryByText("Something went wrong")).toBeNull(); + + viewportSpy.mockRestore(); + }); + it.each([ { name: "mobile landscape", width: 844, height: 390 }, { name: "tablet", width: 834, height: 1112 },