1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { AuthFilesystemReadScope, EnvironmentId, ThreadId } from "@t3tools/contracts";
3import type { ReactNode } from "react";
4import { act, create, type ReactTestRenderer } from "react-test-renderer";
5import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
6
7const state = vi.hoisted(() => ({
8 canReadFiles: true,
9 isCheckingAccess: false,
10 error: null as string | null,
11 readScope: vi.fn(),
12 openFile: vi.fn(),
13}));
14
15const target = {
16 environmentId: EnvironmentId.make("content-search-secondary"),
17 cwd: "/project",
18 projectName: "Project",
19 threadRef: scopeThreadRef(
20 EnvironmentId.make("content-search-secondary"),
21 ThreadId.make("content-search-thread"),
22 ),
23};
24
25vi.mock("~/hooks/useActiveProjectTarget", () => ({ useActiveProjectTarget: () => target }));
26vi.mock("~/hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "light" }) }));
27vi.mock("~/rightPanelStore", () => ({
28 useRightPanelStore: { getState: () => ({ openFile: state.openFile }) },
29}));
30vi.mock("~/state/session", () => ({ readEnvironmentScope: state.readScope }));
31vi.mock("~/state/queries", () => ({
32 useProjectContentSearch: ({ query }: { query: string }) => ({
33 canReadFiles: state.canReadFiles,
34 isCheckingAccess: state.isCheckingAccess,
35 error: state.error,
36 isPending: state.isCheckingAccess,
37 hasQuery: query.length > 0,
38 truncated: false,
39 invalidRegex: false,
40 matches:
41 state.canReadFiles && query.length > 0
42 ? [
43 {
44 resultIndex: 0,
45 path: "src/index.ts",
46 lineNumber: 3,
47 lineContent: "match",
48 matchRanges: [],
49 },
50 ]
51 : [],
52 }),
53}));
54vi.mock("../CommandPaletteContent", () => ({ CommandPaletteContent: "section" }));
55vi.mock("../chat/PierreEntryIcon", () => ({ PierreEntryIcon: () => null }));
56vi.mock("./HighlightedSearchLine", () => ({ HighlightedSearchLine: () => null }));
57vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" }));
58vi.mock("../ui/toggle", () => ({ Toggle: "button" }));
59vi.mock("../ui/tooltip", () => ({
60 Tooltip: "div",
61 TooltipPopup: "span",
62 TooltipTrigger: ({ render }: { render: ReactNode }) => render,
63}));
64
65import { ProjectContentSearchDialog } from "./ProjectContentSearchDialog";
66
67let renderer: ReactTestRenderer | undefined;
68const onOpenChange = vi.fn();
69
70beforeEach(() => {
71 state.canReadFiles = true;
72 state.isCheckingAccess = false;
73 state.error = null;
74 state.readScope.mockReset().mockReturnValue(true);
75 state.openFile.mockClear();
76 onOpenChange.mockClear();
77 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
78 vi.stubGlobal("document", { querySelector: () => null });
79});
80
81afterEach(async () => {
82 await act(() => renderer?.unmount());
83 vi.unstubAllGlobals();
84});
85
86async function openSearch() {
87 await act(() => {
88 renderer = create(<ProjectContentSearchDialog onOpenChange={onOpenChange} />);
89 });
90 return renderer!.root;
91}
92
93it("enables search after access resolves and opens a result in its own environment", async () => {
94 state.canReadFiles = false;
95 state.isCheckingAccess = true;
96 const root = await openSearch();
97 expect(root.findByType("section").props.inputProps.disabled).toBe(true);
98 expect(
99 root.findAllByType("div").some((node) => node.children.includes("Checking file access…")),
100 ).toBe(true);
101
102 state.canReadFiles = true;
103 state.isCheckingAccess = false;
104 await act(() => renderer!.update(<ProjectContentSearchDialog onOpenChange={onOpenChange} />));
105 expect(root.findByType("section").props.inputProps.disabled).toBe(false);
106 await act(() => root.findByType("section").props.onValueChange("match"));
107 await act(() => root.findByProps({ "data-content-search-result": 0 }).props.onClick());
108
109 expect(state.readScope).toHaveBeenCalledWith(target.environmentId, AuthFilesystemReadScope);
110 expect(state.openFile).toHaveBeenCalledWith(target.threadRef, "src/index.ts", 3);
111 expect(onOpenChange).toHaveBeenCalledWith(false);
112});
113
114it.each(["pointer", "keyboard"])(
115 "rechecks access before a retained %s action opens a result",
116 async (action) => {
117 const root = await openSearch();
118 await act(() => root.findByType("section").props.onValueChange("match"));
119 const openResult = root.findByProps({ "data-content-search-result": 0 }).props.onClick;
120 const onKeyDown = root.findByType("section").props.inputProps.onKeyDown;
121
122 state.readScope.mockReturnValue(false);
123 await act(() => {
124 if (action === "pointer") openResult();
125 else onKeyDown({ key: "Enter", preventDefault() {} });
126 });
127
128 expect(state.openFile).not.toHaveBeenCalled();
129 expect(onOpenChange).not.toHaveBeenCalled();
130 },
131);