175 lines
6.5 KiB
TypeScript
175 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
import { renderHook, render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
import { TooltipProvider } from "@kandev/ui/tooltip";
|
|
import type { ReactElement } from "react";
|
|
|
|
vi.mock("@/components/editors/external-vcs-file-link", () => ({
|
|
ExternalVcsFileLink: (props: Record<string, unknown>) => (
|
|
<span data-testid="external-vcs-file-link-props" data-props={JSON.stringify(props)} />
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
|
|
useResponsiveBreakpoint: () => ({ isMobile: false }),
|
|
}));
|
|
|
|
import { useDiffHeaderToolbar } from "./diff-header-toolbar";
|
|
|
|
const FILE = "src/foo.ts";
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
beforeAll(() => {
|
|
// happy-dom's clipboard implementation is partial — stub writeText so the
|
|
// copy-diff handler can be invoked without throwing.
|
|
Object.defineProperty(navigator, "clipboard", {
|
|
value: { writeText: vi.fn().mockResolvedValue(undefined) },
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
function renderToolbar(node: ReactElement) {
|
|
return render(<TooltipProvider>{node}</TooltipProvider>);
|
|
}
|
|
|
|
/**
|
|
* Build a synthetic FileDiffMetadata shape. Calling this through the
|
|
* hook callback means a future library change to the metadata shape (rename,
|
|
* removal, or restructure) surfaces here rather than only at the live FileDiff
|
|
* render site.
|
|
*/
|
|
function headerProp(name: string | undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (name === undefined ? {} : { name }) as any;
|
|
}
|
|
|
|
function externalLinkProps() {
|
|
return JSON.parse(screen.getByTestId("external-vcs-file-link-props").dataset.props ?? "{}");
|
|
}
|
|
|
|
const baseOpts = {
|
|
filePath: FILE,
|
|
diff: `diff --git a/${FILE} b/${FILE}\n--- a/${FILE}\n+++ b/${FILE}\n@@ -1,1 +1,1 @@\n-a\n+b`,
|
|
wordWrap: false,
|
|
onToggleWordWrap: vi.fn(),
|
|
viewMode: "split" as const,
|
|
onToggleViewMode: vi.fn(),
|
|
};
|
|
|
|
describe("useDiffHeaderToolbar", () => {
|
|
it("returns a callback that renders the toolbar buttons", () => {
|
|
const { result } = renderHook(() => useDiffHeaderToolbar(baseOpts));
|
|
// The callback receives FileDiffMetadata directly; passing a synthetic
|
|
// value through the hook means a future library change to that shape
|
|
// surfaces here instead of only at the live FileDiff render site.
|
|
const node = result.current(headerProp(FILE));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
expect(screen.getByLabelText("Copy diff")).toBeTruthy();
|
|
expect(screen.getByLabelText("Toggle word wrap")).toBeTruthy();
|
|
expect(screen.getByLabelText("Switch to unified view")).toBeTruthy();
|
|
});
|
|
|
|
it("wires the copy-diff button to navigator.clipboard.writeText", () => {
|
|
const { result } = renderHook(() => useDiffHeaderToolbar(baseOpts));
|
|
const node = result.current(headerProp(FILE));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
fireEvent.click(screen.getByLabelText("Copy diff"));
|
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(baseOpts.diff);
|
|
});
|
|
|
|
it("invokes onToggleWordWrap and onToggleViewMode when their buttons are clicked", () => {
|
|
const onToggleWordWrap = vi.fn();
|
|
const onToggleViewMode = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useDiffHeaderToolbar({ ...baseOpts, onToggleWordWrap, onToggleViewMode }),
|
|
);
|
|
const node = result.current(headerProp(FILE));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
fireEvent.click(screen.getByLabelText("Toggle word wrap"));
|
|
fireEvent.click(screen.getByLabelText("Switch to unified view"));
|
|
expect(onToggleWordWrap).toHaveBeenCalledTimes(1);
|
|
expect(onToggleViewMode).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("prefers the path from the library callback's fileDiff.name over the opts filePath", () => {
|
|
const onOpenFile = vi.fn();
|
|
const { result } = renderHook(() => useDiffHeaderToolbar({ ...baseOpts, onOpenFile }));
|
|
const node = result.current(headerProp("src/renamed.ts"));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
fireEvent.click(screen.getByLabelText("Edit"));
|
|
expect(onOpenFile).toHaveBeenCalledWith("src/renamed.ts", undefined);
|
|
});
|
|
|
|
it("forwards the repo subpath to onOpenFile so multi-repo files open under the right repository", () => {
|
|
const onOpenFile = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useDiffHeaderToolbar({ ...baseOpts, onOpenFile, repo: "enrichment-commons" }),
|
|
);
|
|
const node = result.current(headerProp("src/foo.ts"));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
fireEvent.click(screen.getByLabelText("Edit"));
|
|
expect(onOpenFile).toHaveBeenCalledWith("src/foo.ts", "enrichment-commons");
|
|
});
|
|
|
|
it("falls back to opts.filePath when the callback prop has no fileDiff.name", () => {
|
|
const onOpenFile = vi.fn();
|
|
const { result } = renderHook(() => useDiffHeaderToolbar({ ...baseOpts, onOpenFile }));
|
|
const node = result.current(headerProp(undefined));
|
|
renderToolbar(node as ReactElement);
|
|
|
|
fireEvent.click(screen.getByLabelText("Edit"));
|
|
expect(onOpenFile).toHaveBeenCalledWith(FILE, undefined);
|
|
});
|
|
|
|
it("shows the markdown preview button only for .md/.mdx files", () => {
|
|
const onPreviewMarkdown = vi.fn();
|
|
const { result } = renderHook(() => useDiffHeaderToolbar({ ...baseOpts, onPreviewMarkdown }));
|
|
const tsNode = result.current(headerProp(FILE));
|
|
const { unmount } = renderToolbar(tsNode as ReactElement);
|
|
expect(screen.queryByLabelText("Preview markdown")).toBeNull();
|
|
unmount();
|
|
|
|
const mdNode = result.current(headerProp("README.md"));
|
|
renderToolbar(mdNode as ReactElement);
|
|
expect(screen.getByLabelText("Preview markdown")).toBeTruthy();
|
|
});
|
|
|
|
it("forwards file, repository, revision, and rename context to the external action", () => {
|
|
const { result } = renderHook(() =>
|
|
useDiffHeaderToolbar({
|
|
...baseOpts,
|
|
sessionId: "session-1",
|
|
taskId: "task-1",
|
|
repositoryId: "repo-1",
|
|
repo: "frontend",
|
|
status: "renamed",
|
|
publishedBranch: "feature/external-links",
|
|
baseBranch: "main",
|
|
}),
|
|
);
|
|
const node = result.current({
|
|
...headerProp("src/new-name.ts"),
|
|
prevName: "src/old-name.ts",
|
|
});
|
|
renderToolbar(node as ReactElement);
|
|
|
|
expect(externalLinkProps()).toEqual({
|
|
filePath: "src/new-name.ts",
|
|
previousPath: "src/old-name.ts",
|
|
status: "renamed",
|
|
taskId: "task-1",
|
|
sessionId: "session-1",
|
|
repositoryId: "repo-1",
|
|
repositoryName: "frontend",
|
|
publishedBranch: "feature/external-links",
|
|
baseBranch: "main",
|
|
size: "xs",
|
|
});
|
|
});
|
|
});
|