Files
kandev/apps/web/components/task/changes-panel-timeline-grouping.test.tsx
T
Kandev Agent 7c00767b6e feat(web/changes-panel): per-repo group headers in the dockview Changes tab
The Changes tab in the dockview panel (the Files / Changes (N) tab the
user actually interacts with — not the standalone Review dialog) was
still rendering a flat file list, so multi-repo task changes either
collided onto the legacy single-status (showing only one repo's files)
or rendered without grouping.

- FileInfo gains repository_name (optional, single-repo unchanged).
- useSessionGit aggregates files from useSessionGitStatusByRepo when
  multi-repo data is present, stamping each FileInfo with its
  repository_name. Falls back to the legacy single status for
  single-repo workspaces. Aggregation extracted into
  aggregateFilesAcrossRepos to keep useSessionGit under the 100-line
  function lint cap.
- ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries
  repositoryName through to the renderer.
- FileListSection delegates to a new FileListBody that groups rows by
  repositoryName; renders a small per-repo subheader when 2+ repos OR
  any single named repo is present, otherwise falls back to the
  pre-multi-repo flat list (same DOM shape).
- Tests cover: no header for plain (no repo_name) files, headers
  per repo for 2+ groups, single named repo still gets a label.

Restart the backend so the per-repo trackers spawn for in-flight
multi-repo tasks; new launches are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 10:47:14 +01:00

106 lines
2.9 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import type { ComponentProps } from "react";
vi.mock("./changes-panel-file-row", () => ({
FileRow: ({ file }: { file: { path: string } }) => (
<li data-testid="file-row">{file.path}</li>
),
BulkActionBar: () => null,
DefaultActionButtons: () => null,
}));
vi.mock("./commit-row", () => ({
CommitRow: () => null,
}));
vi.mock("@/hooks/use-multi-select", () => ({
useMultiSelect: () => ({
selectedPaths: new Set<string>(),
isSelected: () => false,
handleClick: vi.fn(),
clearSelection: vi.fn(),
}),
}));
import { FileListSection } from "./changes-panel-timeline";
afterEach(cleanup);
type Props = ComponentProps<typeof FileListSection>;
const baseProps: Omit<Props, "files" | "variant" | "isLast" | "actionLabel" | "onAction"> = {
pendingStageFiles: new Set(),
onOpenDiff: vi.fn(),
onEditFile: vi.fn(),
onStage: vi.fn(),
onUnstage: vi.fn(),
onDiscard: vi.fn(),
};
function file(path: string, repo?: string): Props["files"][number] {
return {
path,
status: "modified",
staged: false,
plus: 1,
minus: 0,
oldPath: undefined,
repositoryName: repo,
};
}
describe("FileListSection — multi-repo grouping", () => {
it("renders no repo header when files lack a repository_name (single-repo)", () => {
render(
<FileListSection
{...baseProps}
variant="unstaged"
isLast={false}
actionLabel="Stage all"
onAction={() => undefined}
files={[file("a.ts"), file("b.ts")]}
/>,
);
expect(screen.queryAllByTestId("changes-repo-header")).toHaveLength(0);
expect(screen.getAllByTestId("file-row")).toHaveLength(2);
});
it("renders one header per repo when 2+ repos are present", () => {
render(
<FileListSection
{...baseProps}
variant="unstaged"
isLast={false}
actionLabel="Stage all"
onAction={() => undefined}
files={[
file("src/app.tsx", "frontend"),
file("src/api.ts", "frontend"),
file("handlers/task.go", "backend"),
]}
/>,
);
const headers = screen.getAllByTestId("changes-repo-header");
expect(headers).toHaveLength(2);
expect(headers[0].textContent).toContain("frontend");
expect(headers[0].textContent).toContain("2");
expect(headers[1].textContent).toContain("backend");
expect(headers[1].textContent).toContain("1");
});
it("shows a header for a single named repo too", () => {
render(
<FileListSection
{...baseProps}
variant="unstaged"
isLast={false}
actionLabel="Stage all"
onAction={() => undefined}
files={[file("a.ts", "only-repo")]}
/>,
);
expect(screen.getByTestId("changes-repo-header").textContent).toContain("only-repo");
});
});