Files
kandev/apps/web/components/editors/codemirror/use-codemirror-walkthrough-range.test.ts
T
2026-07-28 16:38:56 +01:00

119 lines
3.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderHook } from "@testing-library/react";
import type { EditorView } from "@codemirror/view";
const hookState = vi.hoisted(() => ({
tasks: { activeTaskId: "task-1" },
walkthroughs: {
activeStepByTaskId: { "task-1": 0 },
byTaskId: {
"task-1": {
steps: [
{
file: "walkthrough_a.txt",
line: 2,
line_end: 3,
text: "Explain this range",
},
],
},
},
},
}));
const anchorMocks = vi.hoisted(() => ({
clear: vi.fn(),
isVisible: vi.fn(() => true),
set: vi.fn(),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: typeof hookState) => unknown) => selector(hookState),
}));
vi.mock("@/lib/walkthrough-open-state", () => ({
useIsWalkthroughOpenForTask: () => true,
}));
vi.mock("@/lib/walkthrough-editor-anchor", () => ({
clearWalkthroughEditorAnchor: anchorMocks.clear,
isWalkthroughAnchorTargetVisible: anchorMocks.isVisible,
setWalkthroughEditorAnchor: anchorMocks.set,
}));
vi.mock("@codemirror/view", () => ({
EditorView: { scrollIntoView: vi.fn() },
}));
import { useCodeMirrorWalkthroughRange } from "./use-codemirror-walkthrough-range";
function rect(left: number, top: number, width: number, height: number): DOMRect {
return { left, top, width, height, right: left + width, bottom: top + height } as DOMRect;
}
function createView(): EditorView {
const dom = document.createElement("div");
document.body.append(dom);
vi.spyOn(dom, "getBoundingClientRect").mockReturnValue(rect(20, 30, 500, 300));
return {
coordsAtPos: (position: number) =>
position < 30 ? { left: 24, top: 70, bottom: 90 } : { left: 24, top: 90, bottom: 110 },
dispatch: vi.fn(),
dom,
scrollDOM: dom,
state: {
doc: {
line: (line: number) => ({ from: line * 10, to: line * 10 + 5 }),
lines: 3,
},
},
} as unknown as EditorView;
}
beforeEach(() => {
anchorMocks.clear.mockClear();
anchorMocks.isVisible.mockClear();
anchorMocks.isVisible.mockReturnValue(true);
anchorMocks.set.mockClear();
});
afterEach(() => {
vi.unstubAllGlobals();
document.body.replaceChildren();
});
describe("useCodeMirrorWalkthroughRange", () => {
it("preserves the range box but clears the anchor when its target is occluded", () => {
const view = createView();
const area = document.createElement("div");
document.body.append(area);
const editorAreaRef = { current: area };
vi.spyOn(area, "getBoundingClientRect").mockReturnValue(rect(0, 0, 600, 400));
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
callback(0);
return 1;
});
vi.stubGlobal("cancelAnimationFrame", vi.fn());
vi.stubGlobal(
"ResizeObserver",
class {
disconnect() {}
observe() {}
},
);
anchorMocks.isVisible.mockReturnValue(false);
const { result } = renderHook(() =>
useCodeMirrorWalkthroughRange({
view,
editorAreaRef,
path: "walkthrough_a.txt",
}),
);
expect(result.current).toMatchObject({ startLine: 2, endLine: 3 });
expect(anchorMocks.clear).toHaveBeenCalledWith("task-1:0::walkthrough_a.txt:cm");
expect(anchorMocks.set).not.toHaveBeenCalled();
});
});