diff --git a/packages/dashboard/app/__tests__/graph-workflow-header.test.tsx b/packages/dashboard/app/__tests__/graph-workflow-header.test.tsx index d4eee2430e..3270b8a805 100644 --- a/packages/dashboard/app/__tests__/graph-workflow-header.test.tsx +++ b/packages/dashboard/app/__tests__/graph-workflow-header.test.tsx @@ -89,11 +89,15 @@ describe("Graph workflow header integration", () => { expect(headerSlot.contains(selector)).toBe(true); const contextTasks = screen.getByTestId("graph-plugin-context-tasks"); + /* + FNXC:GraphWorkflowSelection 2026-07-07-08:15: + FN-7057 changed filterTasksByGraphWorkflowSelection to treat stale taskWorkflowIds entries that reference deleted/missing workflows as DEFAULT-workflow assignments (so tasks never vanish from every view). FN-unknown carries wf-missing, so under the default Coding selection it now resolves to the default workflow and is SHOWN, not filtered out. + */ await waitFor(() => { expect(within(contextTasks).getByTestId("graph-context-task-FN-default")).toBeInTheDocument(); expect(within(contextTasks).getByTestId("graph-context-task-FN-unassigned")).toBeInTheDocument(); expect(within(contextTasks).queryByTestId("graph-context-task-FN-review")).toBeNull(); - expect(within(contextTasks).queryByTestId("graph-context-task-FN-unknown")).toBeNull(); + expect(within(contextTasks).getByTestId("graph-context-task-FN-unknown")).toBeInTheDocument(); }); fireEvent.click(selector); diff --git a/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts b/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts index 8e148a0588..7581933b57 100644 --- a/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts +++ b/packages/dashboard/app/components/__tests__/EngineControlMenu.css.test.ts @@ -90,6 +90,28 @@ function extractMobileMediaBlocks(css: string): string { return blocks.join("\n"); } +function extractMediaBlocksForWidth(css: string, width: string): string { + const blocks: string[] = []; + const regex = new RegExp(`@media[^{}]*\\(max-width:\\s*${width}\\)[^{]*\\{`, "g"); + let match: RegExpExecArray | null; + + while ((match = regex.exec(css)) !== null) { + const startIdx = match.index + match[0].length; + let braceCount = 1; + let endIdx = startIdx; + while (braceCount > 0 && endIdx < css.length) { + if (css[endIdx] === "{") braceCount += 1; + if (css[endIdx] === "}") braceCount -= 1; + endIdx += 1; + } + if (braceCount === 0) { + blocks.push(css.slice(startIdx, endIdx - 1)); + } + } + + return blocks.join("\n"); +} + function normalizeCss(css: string): string { return css.replace(/\s+/g, " ").trim(); } @@ -132,7 +154,11 @@ describe("EngineControlMenu CSS token validity (FN-6862)", () => { it("renders the mobile and narrow-tablet footer popover as a viewport-safe bottom panel", () => { expect(componentCss).toContain("@media (max-width: 1024px)"); - expect(componentCss).not.toContain("@media (max-width: 768px)"); + /* + FNXC:EngineControls 2026-07-07-08:20: + FN-7340 added a @media (max-width: 768px) block for range-slider thumb touch-target sizing — a separate concern from popover placement, which stays on the 1024px breakpoint (extractMobileMediaBlocks). The earlier whole-file ban on "@media (max-width: 768px)" was too broad; assert instead that the 768px block never repositions the popover. + */ + expect(extractMediaBlocksForWidth(componentCss, "768px")).not.toContain(".engine-control-menu__popover"); const desktopPopoverBlock = normalizeCss(extractRuleBlock(componentCss, ".engine-control-menu__popover")); const footerDesktopPopoverBlock = normalizeCss( diff --git a/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx b/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx index 942b64aa7f..9caaf8eeb1 100644 --- a/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx +++ b/packages/dashboard/app/components/__tests__/MissionManager.delete-confirm.test.tsx @@ -227,6 +227,12 @@ describe("MissionManager mission delete confirmation", () => { const addToast = vi.fn(); renderMissionManager(addToast); + /* + FNXC:MissionManager 2026-07-07-08:25: + FN-7156 made inline Missions open on the overview (no first-mission auto-selection), so a selected-detail delete must first click the mission in the list to load its detail before the fetchMission/delete-modal flow runs. + */ + fireEvent.click(await findMissionListItem("Build Auth System")); + await waitFor(() => { expect(mockFetchMission).toHaveBeenCalledWith("M-001", projectId); }); diff --git a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx index 976a4dfae5..1489635d8c 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx @@ -127,7 +127,7 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { vi.unstubAllGlobals(); }); - it("normalizes scrollLeft to 0 on initial mobile render and keeps snap style in CSS, not inline", () => { + it("preserves board column scroll during initial mobile stabilization while keeping snap style in CSS, not inline", () => { const viewportSpy = mockViewport(375); const raf = vi.fn<(cb: FrameRequestCallback) => number>((cb) => { setTimeout(() => cb(0), 0); @@ -145,14 +145,18 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { act(() => { vi.runOnlyPendingTimers(); }); - expect(board.scrollLeft).toBe(0); + /* + FNXC:BoardMobile 2026-07-07-08:30: + FN-7342 (preserve board scroll during refresh stabilization) removed the `boardEl.scrollLeft = 0` reset from mobile stabilization — #board is the user's horizontal scroller, so stabilization now only normalizes document-level horizontal drift and must not force the board back to triage. The board's column scroll position is therefore preserved (500) instead of reset to 0; rAF scheduling and the CSS-not-inline snap invariant still hold. + */ + expect(board.scrollLeft).toBe(500); expect(raf).toHaveBeenCalled(); expect(board.style.scrollSnapType).toBe(""); viewportSpy.mockRestore(); }); - it("re-anchors on pageshow persisted restore for mobile", () => { + it("preserves board column scroll on pageshow persisted restore for mobile", () => { const viewportSpy = mockViewport(375); vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => { setTimeout(() => cb(0), 0); @@ -176,7 +180,11 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { act(() => { vi.runOnlyPendingTimers(); }); - expect(board.scrollLeft).toBe(0); + /* + FNXC:BoardMobile 2026-07-07-08:32: + FN-7342 keeps pageshow/bfcache stabilization scoped to document-level drift, so the board column scroll set before the restore (500) is preserved rather than re-anchored to 0. + */ + expect(board.scrollLeft).toBe(500); viewportSpy.mockRestore(); }); @@ -468,7 +476,11 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { expect(document.querySelector("main.board.board-workflow-columns")).not.toBeNull(); }); - expect(document.querySelector(".board-workflow-toolbar")).toBeNull(); + /* + FNXC:BoardMobile 2026-07-07-08:35: + FN-6825 (combine workflow actions in switcher) moved edit/create into the WorkflowSwitcher, so shouldRenderWorkflowControls is now `workflowOptions.length > 0` rather than gated on onCreateWorkflow/onOpenWorkflowEditor. A Board rendered without those callbacks still shows the switcher toolbar whenever workflow options exist, so the no-callbacks toolbar shell no longer disappears. + */ + expect(document.querySelector(".board-workflow-toolbar")).not.toBeNull(); expect(document.querySelectorAll(".board-workflow-columns [data-testid^='column-']")).toHaveLength(6); viewportSpy.mockRestore(); diff --git a/packages/dashboard/app/components/__tests__/workflow-auto-layout.test.ts b/packages/dashboard/app/components/__tests__/workflow-auto-layout.test.ts index 32a6bacbbc..641554dd17 100644 --- a/packages/dashboard/app/components/__tests__/workflow-auto-layout.test.ts +++ b/packages/dashboard/app/components/__tests__/workflow-auto-layout.test.ts @@ -236,11 +236,15 @@ describe("autoLayout — foreach / unreachable / cycles", () => { "code-review", "review", ]); + /* + FNXC:WorkflowAutoLayout 2026-07-07-08:10: + FN-7265 (align per-step review workflow) removed the standalone `review` prompt node from the stepwise IR — per-step review now happens inside the foreach (step-review), so the post-foreach success path is steps → browser-verification → code-review → completion-summary → merge-gate. The connected editor run for the stepwise built-in therefore ends at `completion-summary`, not `review`. + */ assertAutoLayoutRunConnected("stepwise", workflowDef(BUILTIN_STEPWISE_CODING_WORKFLOW_IR), [ "steps", "browser-verification", "code-review", - "review", + "completion-summary", ]); });