08f07b709e
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
import type { ComponentProps } from "react";
|
|
|
|
vi.mock("./changes-panel-file-row", () => ({
|
|
FileRow: ({ file, indentPx }: { file: { path: string }; indentPx?: number }) => (
|
|
<li data-testid="file-row" data-indent={indentPx ?? 0}>
|
|
{file.path}
|
|
</li>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/lib/state/dockview-store", () => ({
|
|
useDockviewStore: () => null,
|
|
}));
|
|
|
|
import { ChangesTree, RepoTreeGroup } from "./changes-panel-tree";
|
|
|
|
afterEach(cleanup);
|
|
|
|
type Props = ComponentProps<typeof ChangesTree>;
|
|
|
|
const stubMultiSelect: Props["multiSelect"] = {
|
|
selectedPaths: new Set<string>(),
|
|
isSelected: () => false,
|
|
handleClick: vi.fn(() => false),
|
|
clearSelection: vi.fn(),
|
|
selectAll: vi.fn(),
|
|
setSelectedPaths: vi.fn(),
|
|
};
|
|
|
|
const baseProps: Omit<Props, "files" | "variant"> = {
|
|
pendingStageFiles: new Set(),
|
|
onOpenDiff: vi.fn(),
|
|
onEditFile: vi.fn(),
|
|
onStage: vi.fn(),
|
|
onUnstage: vi.fn(),
|
|
onDiscard: vi.fn(),
|
|
multiSelect: stubMultiSelect,
|
|
};
|
|
|
|
function file(path: string): Props["files"][number] {
|
|
return {
|
|
path,
|
|
status: "modified",
|
|
staged: false,
|
|
plus: 1,
|
|
minus: 0,
|
|
oldPath: undefined,
|
|
};
|
|
}
|
|
|
|
const APPS_WEB_DIR = "tree-dir-apps-web";
|
|
const FOO_TS = "apps/web/foo.ts";
|
|
const BAR_TS = "apps/web/bar.ts";
|
|
|
|
describe("ChangesTree", () => {
|
|
it("renders folders as dir rows and files as file rows", () => {
|
|
render(
|
|
<ChangesTree
|
|
{...baseProps}
|
|
variant="unstaged"
|
|
files={[file(FOO_TS), file(BAR_TS), file("README.md")]}
|
|
/>,
|
|
);
|
|
// Two file rows under apps/web plus README at root.
|
|
expect(screen.getAllByTestId("file-row")).toHaveLength(3);
|
|
// A directory row exists for "apps/web" (chain-collapsed).
|
|
const dir = screen.getByTestId(APPS_WEB_DIR);
|
|
expect(dir.textContent).toContain("apps/web");
|
|
});
|
|
|
|
it("collapses a single-child dir chain into one row", () => {
|
|
render(<ChangesTree {...baseProps} variant="unstaged" files={[file("a/b/c/file.ts")]} />);
|
|
// The chain a/b/c renders as a single dir row keyed by the deepest path.
|
|
const dir = screen.getByTestId("tree-dir-a-b-c");
|
|
expect(dir.textContent).toContain("a/b/c");
|
|
});
|
|
|
|
it("hides children when a folder is collapsed", () => {
|
|
render(<ChangesTree {...baseProps} variant="unstaged" files={[file(FOO_TS), file(BAR_TS)]} />);
|
|
expect(screen.getAllByTestId("file-row")).toHaveLength(2);
|
|
fireEvent.click(screen.getByTestId(APPS_WEB_DIR));
|
|
expect(screen.queryAllByTestId("file-row")).toHaveLength(0);
|
|
});
|
|
|
|
it("uses the parent multiSelect (not an internal one) so bulk actions stay wired", () => {
|
|
// Regression: tree mode used to instantiate its own useMultiSelect, which
|
|
// left the section-level BulkActionBar blind to tree selections. Verify
|
|
// the prop is consulted by checking that isSelected/handleClick come from
|
|
// the supplied object.
|
|
const isSelected = vi.fn(() => true);
|
|
const handleClick = vi.fn(() => false);
|
|
render(
|
|
<ChangesTree
|
|
{...baseProps}
|
|
multiSelect={{ ...stubMultiSelect, isSelected, handleClick }}
|
|
variant="unstaged"
|
|
files={[file(FOO_TS)]}
|
|
/>,
|
|
);
|
|
// Our FileRow mock surfaces isSelected via a data attribute when wired.
|
|
// Even without the attribute, the spy proves the parent's hook ran for
|
|
// this file's path during render.
|
|
expect(isSelected).toHaveBeenCalledWith(FOO_TS);
|
|
});
|
|
|
|
it("skips a file entry without a path instead of crashing the route", () => {
|
|
// Regression: archived-task DB snapshots can replay cumulative-diff
|
|
// entries lacking a `path` field; the tree used to crash with
|
|
// "Cannot read properties of undefined (reading 'split')".
|
|
const pathless = { ...file(FOO_TS), path: undefined as unknown as string };
|
|
render(<ChangesTree {...baseProps} variant="unstaged" files={[pathless, file(BAR_TS)]} />);
|
|
expect(screen.getAllByTestId("file-row")).toHaveLength(1);
|
|
expect(screen.getByTestId("file-row").textContent).toBe(BAR_TS);
|
|
});
|
|
|
|
it("indents root files beneath a repository header", () => {
|
|
render(
|
|
<RepoTreeGroup
|
|
{...baseProps}
|
|
variant="unstaged"
|
|
repositoryName="frontend"
|
|
files={[file("README.md")]}
|
|
collapsed={false}
|
|
onToggle={vi.fn()}
|
|
primaryLabel="Stage all"
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByTestId("file-row").getAttribute("data-indent")).toBe("12");
|
|
});
|
|
});
|