Files
fusion/packages/dashboard/app/__tests__/App.taskDetailFloatingGeometry.test.tsx
gsxdsm b42be87ad1 FN-7493: keep task popups on board layer
Keep task-detail popups below global utility windows while preserving Activity menu visibility.

- Route ordinary task-detail FloatingWindow instances through a lower task-detail z-index band.
- Emit and consume popup geometry-change signals so root-portaled Activity menus stay attached during drag and resize.
- Extend dashboard tests and docs for popup layering and Activity dropdown behavior.

Files changed:
 .changeset/fn-7493-task-popup-layer.md             |  7 +++
 docs/dashboard-guide.md                            |  8 ++-
 docs/settings-reference.md                         |  2 +-
 packages/dashboard/app/App.tsx                     |  4 ++
 .../App.taskDetailFloatingGeometry.test.tsx        | 45 +++++++++++++-
 .../dashboard/app/components/FloatingWindow.tsx    | 31 ++++++++--
 .../dashboard/app/components/TaskDetailModal.css   |  3 +
 .../dashboard/app/components/TaskDetailModal.tsx   | 27 ++++++++-
 .../components/__tests__/FloatingWindow.test.tsx   | 32 +++++++++-
 .../FloatingWindowStack.cross-type.test.tsx        | 48 ++++++++++++---
 .../TaskDetailModal.task-activity-chat.test.tsx    | 68 ++++++++++++++++++++++
 .../app/components/floatingWindowStack.ts          | 22 +++++--
 12 files changed, 272 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-7493

Fusion-Task-Lineage: 5f1ec72c-f8dc-44ee-b303-319e0faac0f9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-04 10:42:19 -07:00

155 lines
5.5 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import { describe, expect, it, beforeEach } from "vitest";
import { TASK_DETAIL_FLOATING_GEOMETRY_KEY } from "../App";
import { FloatingWindow } from "../components/FloatingWindow";
function renderTaskDetailPopup(taskId: string) {
return render(
<FloatingWindow
windowKey={`task-detail-${taskId}`}
title={taskId}
onClose={() => {}}
hideHeader
dragHandleSelector=".task-detail-content--embedded > .modal-header"
className="floating-window--task-detail"
persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY}
layer="task-detail"
>
<div className="task-detail-content--embedded">
<div className="modal-header">{taskId}</div>
<div>Task detail body</div>
</div>
</FloatingWindow>
);
}
describe("task-detail FloatingWindow geometry", () => {
beforeEach(() => {
localStorage.clear();
});
it("uses one stable task-detail persistence key across different task window identities", () => {
expect(TASK_DETAIL_FLOATING_GEOMETRY_KEY).toBe("floating-window:task-detail");
});
it("restores the saved task popup size and position when a different task opens", () => {
localStorage.setItem(
TASK_DETAIL_FLOATING_GEOMETRY_KEY,
JSON.stringify({
size: { width: 684, height: 512 },
position: { x: 144, y: 88 },
}),
);
const first = renderTaskDetailPopup("FN-7459-A");
const firstPanel = screen.getByTestId("floating-window-task-detail-FN-7459-A");
expect(firstPanel.style.width).toBe("684px");
expect(firstPanel.style.height).toBe("512px");
expect(firstPanel.style.left).toBe("144px");
expect(firstPanel.style.top).toBe("88px");
first.unmount();
renderTaskDetailPopup("FN-7459-B");
const secondPanel = screen.getByTestId("floating-window-task-detail-FN-7459-B");
expect(secondPanel.style.width).toBe("684px");
expect(secondPanel.style.height).toBe("512px");
expect(secondPanel.style.left).toBe("144px");
expect(secondPanel.style.top).toBe("88px");
expect(secondPanel).toHaveClass("floating-window--task-detail");
});
it("raises multiple task-detail popups within the board layer below utility windows", () => {
render(
<>
<FloatingWindow
windowKey="task-detail-FN-7493-A"
title="FN-7493-A"
onClose={() => {}}
className="floating-window--task-detail"
persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY}
layer="task-detail"
>
<div>task a body</div>
</FloatingWindow>
<FloatingWindow
windowKey="task-detail-FN-7493-B"
title="FN-7493-B"
onClose={() => {}}
className="floating-window--task-detail"
persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY}
layer="task-detail"
>
<div>task b body</div>
</FloatingWindow>
<FloatingWindow windowKey="utility-FN-7493" title="Utility" onClose={() => {}}>
<div>utility body</div>
</FloatingWindow>
</>,
);
const firstTask = screen.getByTestId("floating-window-task-detail-FN-7493-A");
const secondTask = screen.getByTestId("floating-window-task-detail-FN-7493-B");
const utility = screen.getByTestId("floating-window-utility-FN-7493");
expect(Number(secondTask.style.zIndex)).toBeGreaterThan(Number(firstTask.style.zIndex));
expect(Number(secondTask.style.zIndex)).toBeLessThan(Number(utility.style.zIndex));
});
it("keeps task-detail geometry and layer isolated from non-task FloatingWindow keys", () => {
localStorage.setItem(
TASK_DETAIL_FLOATING_GEOMETRY_KEY,
JSON.stringify({
size: { width: 650, height: 490 },
position: { x: 118, y: 76 },
}),
);
localStorage.setItem(
"floating-window:mission-interview",
JSON.stringify({
size: { width: 540, height: 420 },
position: { x: 210, y: 120 },
}),
);
render(
<>
<FloatingWindow
windowKey="task-detail-FN-7459"
title="FN-7459"
onClose={() => {}}
className="floating-window--task-detail"
persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY}
layer="task-detail"
>
<div>task detail body</div>
</FloatingWindow>
<FloatingWindow
windowKey="mission"
title="Mission"
onClose={() => {}}
persistGeometryKey="floating-window:mission-interview"
>
<div>mission body</div>
</FloatingWindow>
</>
);
const taskPanel = screen.getByTestId("floating-window-task-detail-FN-7459");
const taskOverlay = screen.getByTestId("floating-window-overlay-task-detail-FN-7459");
expect(taskPanel.style.width).toBe("650px");
expect(taskPanel.style.height).toBe("490px");
expect(taskPanel.style.left).toBe("118px");
expect(taskPanel.style.top).toBe("76px");
const missionPanel = screen.getByTestId("floating-window-mission");
const missionOverlay = screen.getByTestId("floating-window-overlay-mission");
expect(Number(taskPanel.style.zIndex)).toBeLessThan(Number(missionPanel.style.zIndex));
expect(Number(taskOverlay.style.zIndex)).toBeLessThan(Number(missionOverlay.style.zIndex));
expect(missionPanel.style.width).toBe("540px");
expect(missionPanel.style.height).toBe("420px");
expect(missionPanel.style.left).toBe("210px");
expect(missionPanel.style.top).toBe("120px");
});
});