214a8f5cce
* fix: scope multi-repo file editor open/save to the right repo The editor open/save/sync/delete chain dropped the repository_name in multi-repo task workspaces, so the backend resolved paths against the bare task root and failed with "file not found". Thread the repo subpath through openFile, save, sync, and delete, and store it on FileEditorState. * fix: pass repo from diff Edit button to multi-repo file editor The diff/changes "Edit" button opened files without their repository scope, so multi-repo files resolved against the bare task root and failed with "file not found". Thread repository_name through the diff and review toolbars into onOpenFile. * fix: preserve repo across tab restore and stale syncs Address review feedback: persisted file tabs dropped the repo subpath, so a multi-repo file reopened against the bare task root after a browser refresh. Persist and restore `repo` on StoredFileTab. Also guard the workspace sync against a tab being swapped to a different repo mid-fetch, and widen the remaining onOpenFile type declarations to carry repo. * fix: thread repo into editor panel fallback fetch Address review feedback: FileEditorPanel's useFileLoader fallback fetch (triggered when the open-files map is transiently empty, e.g. after a session switch) called requestFileContent without the repo subpath, re-introducing the multi-repo "file not found" failure. Record repo on the panel params and forward it through useFileLoader. Also assert the full restored-tab shape in the local-storage round-trip test. * fix: guard editor fetches against stale session/repo races Address review feedback: file open and the panel fallback fetch could apply a late response after the user navigated away. openFile now drops the result if the active session changed mid-fetch (via fetchFileEditorState), and the panel loader keys its in-flight guard by session+path+repo so a changed target re-fetches and stale responses are discarded.
128 lines
5.1 KiB
TypeScript
128 lines
5.1 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";
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|