From 52310c9b65cfa330965bb6cd47a64fd80f606949 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 1 Aug 2026 10:19:53 -0700 Subject: [PATCH] FN-8698: test independent Board and List task popups Add regression coverage for independently retained task popups across Board and List views. - Exercise popup identity, refresh, visibility, and close behavior for the same task. - Cover real desktop and mobile Board/List interaction paths. Files changed: .../app/__tests__/App.taskPopupViewGating.test.tsx | 69 ++++++++++++- .../app/components/__tests__/App.test.tsx | 113 ++++++++++++++++++++- 2 files changed, 176 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8698 Fusion-Task-Lineage: 857db4cb-0dd5-45ea-a7b7-9cb95f6f5fef Co-authored-by: Fusion (runfusion.ai) --- .../App.taskPopupViewGating.test.tsx | 69 ++++++++++- .../app/components/__tests__/App.test.tsx | 113 +++++++++++++++++- 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/app/__tests__/App.taskPopupViewGating.test.tsx b/packages/dashboard/app/__tests__/App.taskPopupViewGating.test.tsx index 96c3fd94d0..35279dd7a5 100644 --- a/packages/dashboard/app/__tests__/App.taskPopupViewGating.test.tsx +++ b/packages/dashboard/app/__tests__/App.taskPopupViewGating.test.tsx @@ -1,9 +1,10 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { useState } from "react"; import { describe, expect, it } from "vitest"; import type { Task } from "@fusion/core"; import { isTaskPopupVisibleForView, TASK_DETAIL_FLOATING_GEOMETRY_KEY } from "../App"; import { FloatingWindow } from "../components/FloatingWindow"; -import type { PoppedOutTaskEntry } from "../hooks/usePoppedOutTasks"; +import { usePoppedOutTasks, type PoppedOutTaskEntry } from "../hooks/usePoppedOutTasks"; import type { TaskView } from "../hooks/useViewState"; function task(id: string): Task { @@ -50,6 +51,27 @@ function expectVisibleTaskPopupShell(taskId: string, originTaskView?: TaskView) expect(overlay.className).not.toContain("floating-window-overlay--hidden"); } +/* +FNXC:TaskPopupViewGating 2026-08-01-16:04: +FN-8698 requires Board and List clicks for the same task to address distinct retained popups. +This interaction harness mirrors App's current-view popup callback so it catches any task-ID-only +open or refresh regression while exercising the real FloatingWindow hidden/inert contract. +*/ +function BoardListPopupInteractionHarness({ initialTaskView }: { initialTaskView: "board" | "list" }) { + const [taskView, setTaskView] = useState(initialTaskView); + const { entries, popOut, close } = usePoppedOutTasks(); + const sharedTask = task("FN-8698"); + + return <> + + + + + + + ; +} + const origins: TaskView[] = ["board", "list", "planning", "agents", "command-center", "missions", "documents", "plugin:sample"]; describe("App task popup view gating", () => { @@ -62,9 +84,48 @@ describe("App task popup view gating", () => { expectHiddenTaskPopupShell(entry.task.id, originTaskView); }); - it("reproduces the planning-origin symptom and keeps another non-board/list view scoped", () => { + it.each([["board", "list"], ["list", "board"]] as const)("opens %s then %s popups independently for the same task and closes each without affecting the other", (firstView, secondView) => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Open current task" })); + expectVisibleTaskPopupShell("FN-8698", firstView); + + fireEvent.click(screen.getByRole("button", { name: secondView === "board" ? "Show Board" : "Show List" })); + expectHiddenTaskPopupShell("FN-8698", firstView); + fireEvent.click(screen.getByRole("button", { name: "Open current task" })); + expectVisibleTaskPopupShell("FN-8698", secondView); + expectHiddenTaskPopupShell("FN-8698", firstView); + + // Reopening on the current view refreshes its own instance rather than adding a duplicate. + fireEvent.click(screen.getByRole("button", { name: "Open current task" })); + expect(screen.getAllByTestId(/floating-window-task-detail-FN-8698-(board|list)$/)).toHaveLength(2); + + fireEvent.click(screen.getByRole("button", { name: secondView === "board" ? "Close Board task" : "Close List task" })); + expect(screen.queryByTestId(popupTestId("FN-8698", secondView))).not.toBeInTheDocument(); + expectHiddenTaskPopupShell("FN-8698", firstView); + + fireEvent.click(screen.getByRole("button", { name: firstView === "board" ? "Show Board" : "Show List" })); + expectVisibleTaskPopupShell("FN-8698", firstView); + fireEvent.click(screen.getByRole("button", { name: firstView === "board" ? "Close Board task" : "Close List task" })); + expect(screen.queryByTestId(popupTestId("FN-8698", firstView))).not.toBeInTheDocument(); + }); + + it("keeps independently addressed Board and List popups visible together when view gating is disabled", () => { + render(); + + expectVisibleTaskPopupShell("FN-8698", "board"); + expectVisibleTaskPopupShell("FN-8698", "list"); + }); + + it("keeps another non-board/list view scoped", () => { expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "planning", originTaskView: "planning" })).toBe(true); - expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "agents", originTaskView: "agents" })).toBe(true); expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "agents", originTaskView: "planning" })).toBe(false); }); diff --git a/packages/dashboard/app/components/__tests__/App.test.tsx b/packages/dashboard/app/components/__tests__/App.test.tsx index 9d6da80474..be6be2ba27 100644 --- a/packages/dashboard/app/components/__tests__/App.test.tsx +++ b/packages/dashboard/app/components/__tests__/App.test.tsx @@ -293,9 +293,10 @@ vi.mock("../../components/TaskDetailModal", () => ({ ), - TaskDetailContent: ({ task, onBackToBoard, onOpenDetail }: { task: { id: string; title?: string }; onBackToBoard?: () => void; onOpenDetail?: (task: { id: string; title?: string }) => void }) => ( + TaskDetailContent: ({ task, onBackToBoard, onOpenDetail, onRequestClose }: { task: { id: string; title?: string }; onBackToBoard?: () => void; onOpenDetail?: (task: { id: string; title?: string }) => void; onRequestClose?: () => void }) => (
+ {onRequestClose && }

{task.title ?? task.id}

@@ -696,10 +697,118 @@ import { DEFAULT_BOARD_WORKFLOWS } from "./boardWorkflows.test-helpers"; async function waitForAppShell(): Promise { await waitFor(() => { expect(fetchSettings).toHaveBeenCalled(); - expect(screen.getByTitle("Settings")).toBeTruthy(); + if (mockUseViewportMode() === "mobile") { + expect(screen.getByTestId("mobile-view-toggle")).toBeTruthy(); + } else { + expect(screen.getByTitle("Settings")).toBeTruthy(); + } }); } +describe("FN-8698 retained Board and List task popups", () => { + it.each([ + ["desktop", "board", "list"], + ["desktop", "list", "board"], + ["mobile", "board", "list"], + ["mobile", "list", "board"], + ] as const)("opens %s %s then %s independently through real view affordances", async (viewport, firstView, secondView) => { + /* + FNXC:TaskPopupViewGating 2026-08-01-16:47: + FN-8698 requires the real Board and List callback chain to preserve the origin view in both + directions at desktop and phone breakpoints. A hook-only harness cannot catch a callback that + drops that origin or mobile's separate header navigation path, so this App-level regression + clicks each shipped card/row and view-switch affordance and closes each instance independently. + */ + if (viewport === "mobile") mockUseViewportMode.mockReturnValue("mobile"); + const sharedTask = { + id: "FN-8698", + title: "Retained popup regression task", + description: "Verify Board and List popup identities remain independent.", + column: "todo", + status: "todo", + dependencies: [], + steps: [], + currentStep: 0, + log: [], + createdAt: "2026-08-01T00:00:00.000Z", + updatedAt: "2026-08-01T00:00:00.000Z", + }; + vi.mocked(fetchSettings).mockResolvedValue({ + ...defaultSettings, + openMobileTasksInPopup: true, + taskPopupsBoardListOnly: true, + }); + mockUseTasks.mockImplementation(() => ({ + tasks: [sharedTask], + isStale: false, + createTask: mockCreateTask, + moveTask: vi.fn(), + pauseTask: vi.fn(), + unpauseTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + resetTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + loadArchivedTasks: vi.fn(), + refreshTasks: vi.fn(), + ingestCreatedTasks: vi.fn(), + lastFetchTimeMs: Date.now(), + })); + + render(); + await waitForAppShell(); + + const taskSelector = (view: "board" | "list") => view === "board" + ? '.card[data-id="FN-8698"]' + : viewport === "mobile" + ? '.list-card[data-id="FN-8698"]' + : '.list-row[data-id="FN-8698"]'; + const popupFor = (view: "board" | "list") => `floating-window-task-detail-FN-8698-${view}`; + const overlayFor = (view: "board" | "list") => `floating-window-overlay-task-detail-FN-8698-${view}`; + const showView = async (view: "board" | "list") => { + const navigationTestId = viewport === "mobile" + ? `mobile-view-toggle-${view}` + : `sidebar-nav-${view}`; + fireEvent.click(screen.getByTestId(navigationTestId)); + await waitFor(() => expect(document.querySelector(taskSelector(view))).toBeTruthy()); + }; + + if (firstView !== "board") await showView(firstView); + fireEvent.click(document.querySelector(taskSelector(firstView))!); + await waitFor(() => expect(screen.getByTestId(popupFor(firstView))).toBeTruthy()); + + await showView(secondView); + expect(screen.getByTestId(overlayFor(firstView))).toHaveAttribute("aria-hidden", "true"); + + fireEvent.click(document.querySelector(taskSelector(secondView))!); + await waitFor(() => expect(screen.getByTestId(popupFor(secondView))).toBeTruthy()); + expect(screen.getByTestId(overlayFor(secondView))).not.toHaveAttribute("aria-hidden"); + expect(screen.getByTestId(overlayFor(firstView))).toHaveAttribute("aria-hidden", "true"); + + if (viewport === "mobile") { + fireEvent.click(within(screen.getByTestId(popupFor(secondView))).getByRole("button", { name: "Close" })); + } else { + fireEvent.keyDown(document, { key: "Escape" }); + } + await waitFor(() => expect(screen.queryByTestId(popupFor(secondView))).toBeNull()); + expect(screen.getByTestId(popupFor(firstView))).toBeTruthy(); + + await showView(firstView); + await waitFor(() => expect(screen.getByTestId(overlayFor(firstView))).not.toHaveAttribute("aria-hidden")); + if (viewport === "mobile") { + fireEvent.click(within(screen.getByTestId(popupFor(firstView))).getByRole("button", { name: "Close" })); + } else { + fireEvent.keyDown(document, { key: "Escape" }); + } + await waitFor(() => expect(screen.queryByTestId(popupFor(firstView))).toBeNull()); + }); +}); + beforeEach(() => { vi.clearAllMocks(); __test_clearDashboardViewsCache();