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 c34663ed1d..f6807d6a07 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 @@ -39,6 +39,32 @@ vi.mock("../../api", async (importOriginal) => { return Promise.resolve({ ...mockSettings }); }), fetchWorkflowSteps: vi.fn(() => Promise.resolve([])), + /* + FNXC:WorkflowColumns 2026-07-30-07:45: + The board needs a RESOLVED lane payload. This file builds its mock with + `createDashboardApiMock`, which SPREADS the real module, so `fetchBoardWorkflows` was the real + network call — it never resolves under jsdom, `boardWorkflows` stays null, and Board.tsx:874 holds + `BoardWorkflowSkeleton` ("Loading workflow lanes") with no lanes, task cards or Auto-merge toggle. + Overriding it with the default lifecycle lanes is what a real board sends. + */ + fetchBoardWorkflows: vi.fn(() => Promise.resolve({ + flagEnabled: true, + defaultWorkflowId: "builtin:coding", + workflows: [ + { + id: "builtin:coding", + name: "Coding", + columns: [ + { id: "todo", name: "Todo", flags: { intake: true, hold: true } }, + { id: "in-progress", name: "In Progress", flags: { countsTowardWip: true } }, + { id: "in-review", name: "In Review", flags: { mergeBlocker: true, humanReview: true } }, + { id: "done", name: "Done", flags: { complete: true } }, + { id: "archived", name: "Archived", flags: { archived: true } }, + ], + }, + ], + taskWorkflowIds: {}, + })), fetchAgents: vi.fn(() => Promise.resolve([])), fetchTaskReview: vi.fn(() => Promise.resolve({ diff --git a/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile.test.tsx b/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile.test.tsx index 3160f12f0c..f29baf92b4 100644 --- a/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/auto-merge-toggle-blank.mobile.test.tsx @@ -7,7 +7,40 @@ import { MOBILE_MEDIA_QUERY } from "../../hooks/useViewportMode"; import type { Task } from "@fusion/core"; vi.mock("../../api", () => ({ - fetchBoardWorkflows: vi.fn().mockResolvedValue({ flagEnabled: false, defaultWorkflowId: "", workflows: [], taskWorkflowIds: {} }), + /* + FNXC:WorkflowColumns 2026-07-30-07:10: + The board needs a RESOLVED lane payload or it renders the skeleton forever. Board.tsx:874 shows + `BoardWorkflowSkeleton` whenever `boardWorkflows === null || boardWorkflows.workflows.length === 0`, + so an empty `workflows: []` leaves the board at `data-testid="board-workflows-skeleton"` with no + lanes, no task cards and no Auto-merge toggle — which is why every query here failed with + "Unable to find an accessible element". + + This fixture passed `flagEnabled: false` to get the LEGACY board. U12/R9 deleted that path and + dropped the `flagEnabled` conjunct from the skeleton condition, so the flag now selects nothing and + the empty lane list is all that remains. These are the default lifecycle lanes a real board sends. + + Inlined rather than referencing a module const: `vi.mock` factories are HOISTED above const + declarations, so a named payload above this block fails with "Cannot access before initialization" + and the file reports "no tests". + */ + fetchBoardWorkflows: vi.fn().mockResolvedValue({ + flagEnabled: true, + defaultWorkflowId: "builtin:coding", + workflows: [ + { + id: "builtin:coding", + name: "Coding", + columns: [ + { id: "todo", name: "Todo", flags: { intake: true, hold: true } }, + { id: "in-progress", name: "In Progress", flags: { countsTowardWip: true } }, + { id: "in-review", name: "In Review", flags: { mergeBlocker: true, humanReview: true } }, + { id: "done", name: "Done", flags: { complete: true } }, + { id: "archived", name: "Archived", flags: { archived: true } }, + ], + }, + ], + taskWorkflowIds: {}, + }), fetchWorkflowSteps: vi.fn().mockResolvedValue([]), })); @@ -202,6 +235,25 @@ function installAnimationFrame() { vi.stubGlobal("cancelAnimationFrame", vi.fn()); } +/* +FNXC:WorkflowColumns 2026-07-30-07:30: +Render, then FLUSH the board-workflows promise before asserting. `Board` resolves its lane payload +asynchronously and shows `BoardWorkflowSkeleton` until it lands (Board.tsx:874), so a synchronous +assertion right after `render()` always saw "Loading workflow lanes" and no lanes. These fixtures +used to pass `flagEnabled: false` and get the LEGACY board, which rendered synchronously — U12/R9 +deleted that path, so the await is now required rather than optional. + +Wrapped in `act` so the resulting state update is applied before the queries run, and kept to a +microtask flush (not a timer advance) because this suite runs on fake timers. +*/ +async function renderBoardWithLanes(ui: React.ReactElement) { + const result = render(ui); + await act(async () => { + await Promise.resolve(); + }); + return result; +} + function expectBoardVisible() { expect(document.querySelector("main.board")).not.toBeNull(); expect(screen.getByText("In Review")).toBeInTheDocument(); @@ -219,7 +271,7 @@ describe("auto-merge toggle mobile blank regression", () => { vi.unstubAllGlobals(); }); - it("keeps the mobile board visible after an Android viewport resize triggered by toggling auto-merge", () => { + it("keeps the mobile board visible after an Android viewport resize triggered by toggling auto-merge", async () => { const viewportSpy = mockViewport(375); const visualViewport = createVisualViewport(1); Object.defineProperty(window, "visualViewport", { @@ -228,7 +280,7 @@ describe("auto-merge toggle mobile blank regression", () => { }); installAnimationFrame(); - render(); + await renderBoardWithLanes(); const board = document.querySelector("main.board") as HTMLElement; expect(screen.getByTestId("task-card-FN-5936")).toHaveTextContent("true"); @@ -261,7 +313,7 @@ describe("auto-merge toggle mobile blank regression", () => { viewportSpy.mockRestore(); }); - it("round-trips auto-merge on mobile Android with an empty in-review column without blanking", () => { + it("round-trips auto-merge on mobile Android with an empty in-review column without blanking", async () => { const viewportSpy = mockViewport(375); const visualViewport = createVisualViewport(1); Object.defineProperty(window, "visualViewport", { @@ -270,7 +322,7 @@ describe("auto-merge toggle mobile blank regression", () => { }); installAnimationFrame(); - render(); + await renderBoardWithLanes(); const board = document.querySelector("main.board") as HTMLElement; act(() => { @@ -303,7 +355,7 @@ describe("auto-merge toggle mobile blank regression", () => { viewportSpy.mockRestore(); }); - it("keeps populated task-card and worktree surfaces visible when auto-merge toggles on mobile", () => { + it("keeps populated task-card and worktree surfaces visible when auto-merge toggles on mobile", async () => { const viewportSpy = mockViewport(375); const visualViewport = createVisualViewport(1); Object.defineProperty(window, "visualViewport", { @@ -312,7 +364,7 @@ describe("auto-merge toggle mobile blank regression", () => { }); installAnimationFrame(); - render( + await renderBoardWithLanes( { viewportSpy.mockRestore(); }); - it("re-anchors on the mobile iOS pageshow path after toggling auto-merge", () => { + it("re-anchors on the mobile iOS pageshow path after toggling auto-merge", async () => { const viewportSpy = mockViewport(375); Object.defineProperty(window, "visualViewport", { configurable: true, @@ -345,7 +397,7 @@ describe("auto-merge toggle mobile blank regression", () => { }); installAnimationFrame(); - render(); + await renderBoardWithLanes(); const board = document.querySelector("main.board") as HTMLElement; act(() => { @@ -367,11 +419,11 @@ describe("auto-merge toggle mobile blank regression", () => { viewportSpy.mockRestore(); }); - it("keeps the board visible on tablet where the mobile stabilization effect is disabled", () => { + it("keeps the board visible on tablet where the mobile stabilization effect is disabled", async () => { const viewportSpy = mockViewport(900); installAnimationFrame(); - render(); + await renderBoardWithLanes(); const toggle = screen.getByRole("checkbox", { name: "Auto-merge" }); expect(toggle).toBeChecked(); @@ -384,11 +436,11 @@ describe("auto-merge toggle mobile blank regression", () => { viewportSpy.mockRestore(); }); - it("keeps the board visible on desktop after toggling auto-merge", () => { + it("keeps the board visible on desktop after toggling auto-merge", async () => { const viewportSpy = mockViewport(1280); installAnimationFrame(); - render(); + await renderBoardWithLanes(); fireEvent.click(screen.getByRole("checkbox", { name: "Auto-merge" })); @@ -406,7 +458,7 @@ describe("auto-merge toggle mobile blank regression", () => { }); installAnimationFrame(); - render(); + await renderBoardWithLanes(); const toggle = screen.getByRole("checkbox", { name: "Auto-merge" }); expect(toggle).toBeChecked(); @@ -423,7 +475,7 @@ describe("auto-merge toggle mobile blank regression", () => { viewportSpy.mockRestore(); }); - it("shows a visible page error boundary fallback instead of a blank board when a board child throws", () => { + it("shows a visible page error boundary fallback instead of a blank board when a board child throws", async () => { const viewportSpy = mockViewport(375); const visualViewport = createVisualViewport(1); Object.defineProperty(window, "visualViewport", { @@ -433,7 +485,7 @@ describe("auto-merge toggle mobile blank regression", () => { installAnimationFrame(); const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - render(); + await renderBoardWithLanes(); fireEvent.click(screen.getByRole("checkbox", { name: "Auto-merge" })); diff --git a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx index a798bfe8b8..abba8ce35c 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx @@ -90,14 +90,23 @@ function getMainMobileSection(css: string): string { } describe("mobile board magnetic column snap wiring (FN-8235)", () => { - it("shares the mobile scroll-end hook across legacy, selected, and aggregate live board renders", () => { + /* + FNXC:WorkflowColumns 2026-07-30-08:00: + Re-pinned to TWO board renders, not three. The third was the LEGACY board, deleted by U12/R9 — + `
` no longer appears in Board.tsx at all, so + both the count of 3 and the `toContain` for that markup were pinning removed code. The two that + remain are the selected and aggregate workflow-column renders, and the point of this guard is that + BOTH share the one scroll-snap hook, which still holds. + */ + it("shares the mobile scroll-end hook across the selected and aggregate live board renders", () => { const boardSource = readAppFile("components/Board.tsx"); expect(boardSource).toContain('import { useColumnScrollSnap } from "../hooks/useColumnScrollSnap";'); expect(boardSource).toContain("useColumnScrollSnap(boardElement, { mobileOnly: true });"); - expect(boardSource.match(/ref=\{setBoardRef\}/g)).toHaveLength(3); + expect(boardSource.match(/ref=\{setBoardRef\}/g)).toHaveLength(2); expect(boardSource.match(/className="board board-workflow-columns"/g)).toHaveLength(2); - expect(boardSource).toContain('
'); + // The legacy `
` render is gone; assert it stays gone. + expect(boardSource).not.toContain('
'); }); });