1// @vitest-environment jsdom
2import type { AsyncResult as AsyncResultType } from "effect/reactivity";
3import type { ProjectEntry } from "@t3tools/contracts";
4import { EnvironmentId } from "@t3tools/contracts";
5import { act, useLayoutEffect } from "react";
6import { createRoot, type Root } from "react-dom/client";
7import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
8
9const disk = vi.hoisted(() => ({
10 entries: new Map<string, ProjectEntry[]>(),
11 watchStarts: [] as string[],
12 reads: [] as string[],
13 change: vi.fn<(path: string) => void>(),
14}));
15vi.mock("~/rpc/atomRegistry", async () => {
16 const { AtomRegistry } = await import("effect/reactivity");
17 return { appAtomRegistry: AtomRegistry.make() };
18});
19vi.mock("~/state/projects", async () => {
20 const { AsyncResult, Atom } = await import("effect/reactivity");
21 const { appAtomRegistry } = await import("~/rpc/atomRegistry");
22 const signals = Atom.family((_path: string) =>
23 Atom.make<AsyncResultType.AsyncResult<number>>(AsyncResult.initial()),
24 );
25 const watches = Atom.family((path: string) =>
26 Atom.make((get) => {
27 disk.watchStarts.push(path);
28 return get(signals(path));
29 }),
30 );
31 const listings = Atom.family((path: string) =>
32 Atom.make(() => {
33 disk.reads.push(path);
34 return AsyncResult.success({ entries: disk.entries.get(path) ?? [], truncated: false });
35 }).pipe(Atom.setIdleTTL("5 minutes")),
36 );
37 let revision = 0;
38 disk.change.mockImplementation((path) =>
39 appAtomRegistry.set(signals(path), AsyncResult.success(++revision)),
40 );
41 return {
42 projectEnvironment: {
43 watch: ({ input }: { input: { relativePath: string } }) => watches(input.relativePath),
44 listEntries: ({ input }: { input: { directoryPath: string } }) =>
45 listings(input.directoryPath),
46 },
47 };
48});
49
50import { useDirectoryEntries } from "./useDirectoryEntries";
51import { appAtomRegistry } from "~/rpc/atomRegistry";
52import { projectEnvironment } from "~/state/projects";
53
54let root: Root;
55let container: HTMLDivElement;
56let folders: ReturnType<typeof useDirectoryEntries>;
57function Probe() {
58 const entries = useDirectoryEntries(EnvironmentId.make("directory-watch"), "/workspace");
59 useLayoutEffect(() => {
60 folders = entries;
61 });
62 return <div>{entries.entries.map((entry) => entry.path).join("\n")}</div>;
63}
64beforeEach(() => {
65 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
66 disk.entries.clear();
67 disk.reads.length = 0;
68 disk.watchStarts.length = 0;
69 container = document.createElement("div");
70 document.body.append(container);
71 root = createRoot(container);
72});
73afterEach(async () => {
74 await act(() => root.unmount());
75 appAtomRegistry.reset();
76 container.remove();
77 vi.unstubAllGlobals();
78});
79
80it("starts fresh folder subscriptions and updates expanded contents after host changes", async () => {
81 disk.entries.set("", [{ path: "nested", kind: "directory" }]);
82 disk.entries.set("nested", [{ path: "nested/old.txt", kind: "file" }]);
83 await act(async () => root.render(<Probe />));
84 await act(async () => folders.load("nested"));
85 expect(container.textContent).toContain("nested/old.txt");
86 expect(disk.watchStarts).toEqual([".", "nested"]);
87
88 disk.entries.set("nested", [{ path: "nested/new.txt", kind: "file" }]);
89 await act(async () => disk.change("nested"));
90 expect(container.textContent).toContain("nested/new.txt");
91 expect(container.textContent).not.toContain("nested/old.txt");
92
93 const reads = disk.reads.length;
94 await act(async () => {
95 disk.change("nested");
96 root.unmount();
97 });
98 expect(disk.reads).toHaveLength(reads);
99});
100
101it("refreshes a cached watch revision after registering its cleanup", async () => {
102 disk.entries.set("", [{ path: "fresh.txt", kind: "file" }]);
103 disk.change(".");
104 appAtomRegistry.get(
105 projectEnvironment.watch({
106 environmentId: EnvironmentId.make("directory-watch"),
107 input: { cwd: "/workspace", relativePath: ".", directory: true },
108 }),
109 );
110 await act(async () => root.render(<Probe />));
111 expect(container.textContent).toBe("fresh.txt");
112 expect(disk.watchStarts).toEqual(["."]);
113});