Files
fusion/packages/dashboard/app/components/__tests__/BranchGroupCard.test.tsx
gsxdsm 8376781864 FN-5845: fix mobile agent log scroll and add collapsible branch group card
Improve dashboard mobile log usability and make branch group cards collapsible for cleaner review workflows.

- preserve/restore AgentLogViewer scroll position when toggling mobile drawer and lock body scroll while open
- add collapse/expand state to BranchGroupCard with a summary header and hidden details when collapsed
- add regression tests for AgentLogViewer mobile scroll behavior and BranchGroupCard collapse interactions
- add a changeset and dashboard guide note for the new branch group card behavior

Files changed:
 .changeset/fn-5845-branch-group-collapse.md        |  5 ++
 docs/dashboard-guide.md                            |  1 +
 .../dashboard/app/components/AgentLogViewer.tsx    | 21 ++++++++
 .../dashboard/app/components/BranchGroupCard.css   | 23 ++++++++
 .../dashboard/app/components/BranchGroupCard.tsx   | 63 +++++++++++++++++-----
 .../components/__tests__/AgentLogViewer.test.tsx   | 48 +++++++++++++++++
 .../components/__tests__/BranchGroupCard.test.tsx  | 18 +++++++
 7 files changed, 166 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-5845

Fusion-Task-Lineage: 6a0f2967-5df8-4761-ae6c-50991f01ab78
2026-06-01 16:24:21 -07:00

115 lines
4.6 KiB
TypeScript

import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { BranchGroupCard } from "../BranchGroupCard";
const apiGetBranchGroup = vi.fn();
const apiPromoteBranchGroup = vi.fn();
vi.mock("../../api", () => ({
apiGetBranchGroup: (...args: unknown[]) => apiGetBranchGroup(...args),
apiPromoteBranchGroup: (...args: unknown[]) => apiPromoteBranchGroup(...args),
}));
vi.mock("../../sse-bus", () => ({
subscribeSse: () => () => {},
}));
vi.mock("lucide-react", () => ({
CheckCircle2: () => null,
ChevronDown: () => null,
ChevronRight: () => null,
CircleDashed: () => null,
ExternalLink: () => null,
GitBranch: () => null,
GitPullRequest: () => null,
Loader2: () => null,
}));
function makeGroup(overrides: Record<string, unknown> = {}) {
return {
id: "BG-1",
sourceType: "planning",
sourceId: "PS-1",
branchName: "feature/shared",
autoMerge: false,
prState: "none",
status: "open",
createdAt: Date.now(),
updatedAt: Date.now(),
members: [
{ taskId: "FN-1", title: "one", column: "done", landed: true },
{ taskId: "FN-2", title: "two", column: "in-review", landed: false },
],
completion: { landed: 1, total: 2, complete: false },
...overrides,
};
}
describe("BranchGroupCard", () => {
beforeEach(() => {
apiGetBranchGroup.mockReset();
apiPromoteBranchGroup.mockReset();
});
it("hides promote control while incomplete", async () => {
apiGetBranchGroup.mockResolvedValue({ group: makeGroup() });
render(<BranchGroupCard groupId="BG-1" />);
await screen.findByText("1 of 2 members finished");
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
});
it("shows promote and calls API when complete + autoMerge off", async () => {
apiGetBranchGroup
.mockResolvedValueOnce({ group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }] }) })
.mockResolvedValueOnce({ group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }], prState: "open", prNumber: 22, prUrl: "https://example/pr/22" }) });
apiPromoteBranchGroup.mockResolvedValue({ groupId: "BG-1", prState: "open" });
render(<BranchGroupCard groupId="BG-1" />);
const button = await screen.findByRole("button", { name: /open pr/i });
fireEvent.click(button);
await waitFor(() => {
expect(apiPromoteBranchGroup).toHaveBeenCalledWith("BG-1", undefined);
});
});
it("shows auto-merge badge when complete and autoMerge is on", async () => {
apiGetBranchGroup.mockResolvedValue({
group: makeGroup({
autoMerge: true,
completion: { landed: 2, total: 2, complete: true },
members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }],
}),
});
render(<BranchGroupCard groupId="BG-1" />);
expect(await screen.findByText("Auto-merge enabled")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
});
it("renders tracked group PR link when present", async () => {
apiGetBranchGroup.mockResolvedValue({
group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }], prState: "open", prNumber: 9, prUrl: "https://example/pr/9" }),
});
render(<BranchGroupCard groupId="BG-1" />);
expect(await screen.findByRole("link", { name: /pr #9/i })).toBeInTheDocument();
});
it("shows members by default and collapses via toggle", async () => {
apiGetBranchGroup.mockResolvedValue({ group: makeGroup() });
render(<BranchGroupCard groupId="BG-1" />);
expect(await screen.findByText("FN-1 · one")).toBeInTheDocument();
const toggle = screen.getByRole("button", { name: /collapse branch group/i });
expect(toggle).toHaveAttribute("aria-expanded", "true");
fireEvent.click(toggle);
expect(screen.getByRole("button", { name: /expand branch group/i })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText("FN-1 · one")).toBeNull();
expect(screen.getByText("1 of 2 members finished")).toBeInTheDocument();
});
});