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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-01 10:19:53 -07:00
parent 01d65805c3
commit 52310c9b65
2 changed files with 176 additions and 6 deletions

View File

@@ -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<TaskView>(initialTaskView);
const { entries, popOut, close } = usePoppedOutTasks();
const sharedTask = task("FN-8698");
return <>
<button onClick={() => setTaskView("board")}>Show Board</button>
<button onClick={() => setTaskView("list")}>Show List</button>
<button onClick={() => popOut(sharedTask, taskView)}>Open current task</button>
<button onClick={() => close(sharedTask.id, "board")}>Close Board task</button>
<button onClick={() => close(sharedTask.id, "list")}>Close List task</button>
<PopupGateHarness entries={entries} taskView={taskView} taskPopupsBoardListOnly />
</>;
}
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(<BoardListPopupInteractionHarness initialTaskView={firstView} />);
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(<PopupGateHarness
entries={[
{ task: task("FN-8698"), originTaskView: "board" },
{ task: task("FN-8698"), originTaskView: "list" },
]}
taskView="planning"
taskPopupsBoardListOnly={false}
/>);
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);
});

View File

@@ -293,9 +293,10 @@ vi.mock("../../components/TaskDetailModal", () => ({
</div>
</div>
),
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 }) => (
<section data-testid="main-panel-task-detail">
<button type="button" onClick={onBackToBoard}>Back to board</button>
{onRequestClose && <button type="button" aria-label="Close" onClick={onRequestClose}>Close</button>}
<h2>{task.title ?? task.id}</h2>
<button type="button" onClick={() => onOpenDetail?.({ id: "FN-6965", title: "Nested task" })}>Open nested task</button>
</section>
@@ -696,10 +697,118 @@ import { DEFAULT_BOARD_WORKFLOWS } from "./boardWorkflows.test-helpers";
async function waitForAppShell(): Promise<void> {
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(<App />);
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();