/* Copyright 2026 Marimo. All rights reserved. */
import {
act,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from "@testing-library/react";
import { Provider, createStore } from "jotai";
import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MockRequestClient } from "@/__mocks__/requests";
import { ModalProvider } from "@/components/modal/ImperativeModal";
import { TooltipProvider } from "@/components/ui/tooltip";
import { requestClientAtom } from "@/core/network/requests";
import type { FileInfo, FileSearchResponse } from "@/core/network/types";
import { Deferred } from "@/utils/Deferred";
import type { FilePath } from "@/utils/paths";
import { TreeDndProvider } from "../dnd-wrapper";
import { FileExplorer } from "../file-explorer";
import { openStateAtom } from "../state";
import { fileTreeNodeId, RequestingTree } from "../requesting-tree";
import { useFileSearch } from "../use-file-search";
import { HOVER_EXPAND_DELAY } from "../use-hover-expand";
vi.mock("../file-viewer", () => ({
FileViewer: ({ file }: { file: FileInfo }) =>
Preview: {file.name}
,
}));
function file(name: string, isDirectory = false): FileInfo {
return {
id: name,
name: name.split("/").at(-1) ?? name,
path: `/workspace/${name}`,
isDirectory,
isMarimoFile: false,
};
}
function createDragEvents(clientY = 115) {
const dataTransfer = {
setData: vi.fn(),
getData: vi.fn(),
setDragImage: vi.fn(),
types: [],
dropEffect: "move",
effectAllowed: "all",
};
return (target: Element, type: string) => {
// jsdom has no DragEvent; MouseEvent preserves the pointer coordinates.
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
clientX: 100,
clientY,
});
Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
fireEvent(target, event);
};
}
let store = createStore();
let client: ReturnType;
const wrapper = ({ children }: { children: ReactNode }) => (
{children}
);
beforeEach(() => {
localStorage.clear();
store = createStore();
client = MockRequestClient.create({
getFileRoots: vi.fn().mockResolvedValue({
roots: [{ path: "/workspace", name: "workspace", isPrimary: true }],
}),
sendListFiles: vi.fn().mockImplementation(async ({ path }) => ({
files:
path === "/workspace"
? [file("data", true), file("notes.txt")]
: [file("data/report.csv")],
})),
});
store.set(requestClientAtom, client);
});
describe("file browser navigation", () => {
it("debounces the first character, subsequent edits, and searches after clearing", async () => {
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
vi.useFakeTimers();
try {
fireEvent.change(input, { target: { value: "n" } });
await act(() => vi.advanceTimersByTimeAsync(100));
expect(client.sendSearchFiles).not.toHaveBeenCalled();
fireEvent.change(input, { target: { value: "no" } });
await act(() => vi.advanceTimersByTimeAsync(299));
expect(client.sendSearchFiles).not.toHaveBeenCalled();
await act(() => vi.advanceTimersByTimeAsync(1));
expect(client.sendSearchFiles).toHaveBeenCalledOnce();
expect(client.sendSearchFiles).toHaveBeenLastCalledWith(
expect.objectContaining({ query: "no" }),
);
fireEvent.change(input, { target: { value: "" } });
fireEvent.change(input, { target: { value: "f" } });
await act(() => vi.advanceTimersByTimeAsync(299));
expect(client.sendSearchFiles).toHaveBeenCalledOnce();
await act(() => vi.advanceTimersByTimeAsync(1));
expect(client.sendSearchFiles).toHaveBeenCalledTimes(2);
expect(client.sendSearchFiles).toHaveBeenLastCalledWith(
expect.objectContaining({ query: "f" }),
);
fireEvent.change(input, { target: { value: "" } });
await act(() => vi.advanceTimersByTimeAsync(300));
expect(client.sendSearchFiles).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it("navigates with arrows, opens with Enter, and restores the tree after preview", async () => {
render(, { wrapper });
const tree = await screen.findByRole("tree");
act(() => tree.focus());
expect(document.activeElement).toHaveTextContent("data");
fireEvent.keyDown(document.activeElement!, { key: "ArrowRight" });
expect(client.sendListFiles).toHaveBeenCalledWith({
path: "/workspace/data",
});
expect(await screen.findByText("report.csv")).toBeVisible();
fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
expect(screen.queryByText("Preview: report.csv")).not.toBeInTheDocument();
expect(document.activeElement).toHaveTextContent("report.csv");
fireEvent.keyDown(document.activeElement!, { key: "Enter" });
expect(await screen.findByText("Preview: report.csv")).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "Back to files" }));
expect(await screen.findByText("report.csv")).toBeVisible();
expect(screen.getByRole("tree")).toBe(tree);
await waitFor(() =>
expect(document.activeElement).toHaveTextContent("report.csv"),
);
});
it("bounds mounted rows for a directory with 10,000 files and supports End navigation", async () => {
client.sendListFiles.mockResolvedValue({
root: "/workspace",
files: Array.from({ length: 10_000 }, (_, index) =>
file(`file-${index}.txt`),
),
});
render(, { wrapper });
const tree = await screen.findByRole("tree");
expect(screen.getAllByRole("treeitem").length).toBeLessThan(40);
act(() => tree.focus());
fireEvent.keyDown(document.activeElement!, { key: "End" });
expect(await screen.findByText("file-9999.txt")).toBeVisible();
expect(screen.getAllByRole("treeitem").length).toBeLessThan(40);
});
it("renames with F2 and keeps leaf files distinct from expandable folders", async () => {
render(, { wrapper });
const row = await screen.findByRole("treeitem", { name: "notes.txt" });
expect(row).not.toHaveAttribute("aria-expanded");
expect(screen.getByRole("treeitem", { name: "data" })).toHaveAttribute(
"aria-expanded",
"false",
);
fireEvent.click(screen.getByText("notes.txt"));
fireEvent.keyDown(row, { key: "F2" });
expect(await screen.findByDisplayValue("notes.txt")).toHaveFocus();
fireEvent.keyDown(document.activeElement!, { key: "Escape" });
expect(client.sendRenameFileOrFolder).not.toHaveBeenCalled();
});
it("reveals and expands folders found by search", async () => {
client.sendSearchFiles.mockResolvedValue({
files: [file("data", true)],
query: "data",
totalFound: 1,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "data" } });
await screen.findByText("1 match");
fireEvent.keyDown(input, { key: "Enter" });
expect(await screen.findByText("report.csv")).toBeVisible();
expect(input).toHaveValue("");
expect(screen.getByRole("treeitem", { name: "data" })).toHaveAttribute(
"aria-expanded",
"true",
);
});
it("finds unopened descendants and supports keyboard activation from the search field", async () => {
client.sendSearchFiles.mockResolvedValue({
files: [file("data/report.csv")],
query: "report",
totalFound: 1,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
expect(await screen.findByText("report.csv")).toBeVisible();
expect(client.sendSearchFiles).toHaveBeenCalledWith(
expect.objectContaining({ query: "report", path: "/workspace" }),
);
fireEvent.keyDown(input, { key: "ArrowDown" });
await waitFor(() =>
expect(document.activeElement).toHaveTextContent("report.csv"),
);
fireEvent.keyDown(document.activeElement!, { key: "Enter" });
expect(await screen.findByText("Preview: report.csv")).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "Back to files" }));
expect(input).toHaveValue("report");
fireEvent.keyDown(input, { key: "Escape" });
expect(input).toHaveValue("");
});
it("previews search files without loading their ancestors", async () => {
client.sendSearchFiles.mockResolvedValue({
files: [file("new/nested/report.csv")],
query: "report",
totalFound: 1,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
client.sendListFiles.mockClear();
fireEvent.change(input, { target: { value: "report" } });
await screen.findByText("1 match");
fireEvent.keyDown(input, { key: "Enter" });
expect(await screen.findByText("Preview: report.csv")).toBeVisible();
expect(client.sendListFiles).not.toHaveBeenCalled();
});
it.each([
{ offset: 1, sourceName: "notes.txt", canMove: true },
{ offset: 15, sourceName: "notes.txt", canMove: true },
{ offset: 29, sourceName: "notes.txt", canMove: true },
{ offset: 15, sourceName: "data", canMove: false },
])(
"drops $sourceName at folder offset $offset with canMove=$canMove",
async ({ offset, sourceName, canMove }) => {
render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
const source = screen.getByRole("treeitem", {
name: sourceName,
}).firstElementChild;
if (!source) {
throw new Error("Missing drag source");
}
vi.spyOn(folder, "getBoundingClientRect").mockReturnValue(
new DOMRect(0, 100, 300, 30),
);
const dragEvent = createDragEvents(100 + offset);
dragEvent(source, "dragstart");
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
dragEvent(folder, "dragenter");
dragEvent(folder, "dragover");
await act(async () => {
await new Promise(requestAnimationFrame);
});
dragEvent(folder, "drop");
await waitFor(() => {
expect(client.sendRenameFileOrFolder.mock.calls).toEqual(
canMove
? [
[
{
path: "/workspace/notes.txt",
newPath: "/workspace/data/notes.txt",
},
],
]
: [],
);
});
dragEvent(source, "dragend");
},
);
it("expands hovered folders after a delay and supports nested drops", async () => {
client.sendListFiles.mockImplementation(async ({ path }) => {
if (path === "/workspace") {
return {
root: "/workspace",
files: [file("data", true), file("notes.txt")],
};
}
if (path === "/workspace/data") {
return { root: "/workspace", files: [file("data/nested", true)] };
}
return { root: "/workspace", files: [] };
});
render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
const source = screen.getByRole("treeitem", {
name: "notes.txt",
}).firstElementChild;
if (!source) {
throw new Error("Missing drag source");
}
const dragEvent = createDragEvents();
vi.useFakeTimers();
try {
dragEvent(source, "dragstart");
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
dragEvent(folder, "dragenter");
dragEvent(folder, "dragover");
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY - 1);
});
expect(folder).toHaveAttribute("aria-expanded", "false");
await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(folder).toHaveAttribute("aria-expanded", "true");
expect(client.sendListFiles).toHaveBeenCalledWith({
path: "/workspace/data",
});
const nested = screen.getByRole("treeitem", { name: "nested" });
dragEvent(nested, "dragenter");
dragEvent(nested, "dragover");
await act(async () => {
await vi.advanceTimersByTimeAsync(16);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY);
});
expect(nested).toHaveAttribute("aria-expanded", "true");
expect(client.sendListFiles).toHaveBeenCalledWith({
path: "/workspace/data/nested",
});
await act(async () => {
dragEvent(nested, "drop");
});
expect(client.sendRenameFileOrFolder).toHaveBeenCalledExactlyOnceWith({
path: "/workspace/notes.txt",
newPath: "/workspace/data/nested/notes.txt",
});
dragEvent(source, "dragend");
} finally {
vi.useRealTimers();
}
});
it.each(["leave", "end", "invalid"])(
"does not expand a folder when the drag is %s",
async (reason) => {
render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
const sourceRow =
reason === "invalid"
? folder
: screen.getByRole("treeitem", { name: "notes.txt" });
const source = sourceRow.firstElementChild;
if (!source) {
throw new Error("Missing drag source");
}
const dragEvent = createDragEvents();
vi.useFakeTimers();
try {
dragEvent(source, "dragstart");
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
dragEvent(folder, "dragenter");
dragEvent(folder, "dragover");
await act(async () => {
await vi.advanceTimersByTimeAsync(400);
});
if (reason === "leave") {
dragEvent(sourceRow, "dragenter");
dragEvent(sourceRow, "dragover");
} else if (reason === "end") {
dragEvent(source, "dragend");
}
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
expect(folder).toHaveAttribute("aria-expanded", "false");
expect(client.sendListFiles).not.toHaveBeenCalledWith({
path: "/workspace/data",
});
dragEvent(source, "dragend");
} finally {
vi.useRealTimers();
}
},
);
it("expands nested external upload destinations and cancels when the drag leaves", async () => {
client.sendListFiles.mockImplementation(async ({ path }) => {
if (path === "/workspace") {
return { root: "/workspace", files: [file("data", true)] };
}
if (path === "/workspace/data") {
return { root: "/workspace", files: [file("data/nested", true)] };
}
return { root: "/workspace", files: [] };
});
const { rerender } = render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
vi.useFakeTimers();
try {
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(300);
});
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY);
});
expect(folder).toHaveAttribute("aria-expanded", "false");
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY - 1);
});
expect(folder).toHaveAttribute("aria-expanded", "false");
await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(folder).toHaveAttribute("aria-expanded", "true");
const nested = screen.getByRole("treeitem", { name: "nested" });
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY);
});
expect(nested).toHaveAttribute("aria-expanded", "true");
expect(client.sendListFiles).toHaveBeenCalledWith({
path: "/workspace/data/nested",
});
expect(client.sendRenameFileOrFolder).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("ranks matches across roots before applying the global result limit", async () => {
client.getFileRoots.mockResolvedValue({
roots: [
{ path: "/workspace", name: "workspace", isPrimary: true },
{ path: "/shared", name: "shared", isPrimary: false },
],
});
client.sendSearchFiles.mockImplementation(async ({ path, query }) => {
const files =
path === "/workspace"
? Array.from({ length: 200 }, (_, index) =>
file(`report-${index}.txt`),
)
: [{ ...file("report"), path: "/shared/report" }];
return { files, query, totalFound: files.length };
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
await screen.findByText(/200 matches/);
const results = screen.getAllByRole("treeitem");
expect(results[0]).toHaveAccessibleName("/shared/report");
expect(
client.sendSearchFiles.mock.calls.map(([request]) => request.path),
).toEqual(["/workspace", "/shared"]);
});
it.each([false, true])(
"deduplicates overlapping roots before limiting (primary first: %s)",
async (primaryFirst) => {
const roots = [
{ path: "/workspace", name: "workspace", isPrimary: true },
{ path: "/workspace/data", name: "data", isPrimary: false },
];
client.getFileRoots.mockResolvedValue({
roots: primaryFirst ? roots : roots.toReversed(),
});
const sharedFiles = Array.from({ length: 150 }, (_, index) =>
file(`data/report-${index}.txt`),
);
client.sendSearchFiles.mockImplementation(async ({ path, query }) => {
const files =
path === "/workspace"
? [...sharedFiles, file("report-unique.txt")]
: sharedFiles;
return { files, query, totalFound: files.length };
});
const tree = new RequestingTree({
getRoots: client.getFileRoots,
listFiles: client.sendListFiles,
createFileOrFolder: client.sendCreateFileOrFolder,
deleteFileOrFolder: client.sendDeleteFileOrFolder,
copyFileOrFolder: client.sendCopyFileOrFolder,
renameFileOrFolder: client.sendRenameFileOrFolder,
});
await tree.initialize(vi.fn());
const { result } = renderHook(
() => useFileSearch({ query: "report", tree, showHiddenFiles: false }),
{ wrapper },
);
await waitFor(() => expect(result.current.state.status).toBe("success"));
const state = result.current.state;
if (state.status !== "success") {
throw new Error("Expected search results");
}
expect(state.files).toHaveLength(151);
expect(new Set(state.files.map((file) => file.path)).size).toBe(151);
expect(
state.files.every(
(file) => file.rootPath === "/workspace" && file.isPrimaryRoot,
),
).toBe(true);
expect(
state.files.some(
(file) => file.path === "/workspace/report-unique.txt",
),
).toBe(true);
expect(state.hasMore).toBe(false);
},
);
it("distinguishes same-named search results by their accessible paths", async () => {
client.sendSearchFiles.mockResolvedValue({
query: "report",
files: [file("data/report.csv"), file("archive/report.csv")],
totalFound: 2,
});
render(, { wrapper });
fireEvent.change(
await screen.findByRole("textbox", { name: "Search files and folders" }),
{ target: { value: "report" } },
);
expect(
await screen.findByRole("treeitem", {
name: "/workspace/data/report.csv",
}),
).toBeVisible();
expect(
screen.getByRole("treeitem", { name: "/workspace/archive/report.csv" }),
).toBeVisible();
});
it("shows an error when every search root fails", async () => {
client.sendSearchFiles.mockRejectedValue(new Error("Unavailable"));
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not search files",
);
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
});
it("retries failed hover expansion only after leaving and reentering", async () => {
const { rerender } = render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
client.sendListFiles.mockRejectedValue(new Error("Unavailable"));
client.sendListFiles.mockClear();
vi.useFakeTimers();
try {
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY);
});
expect(folder).toHaveAttribute("aria-expanded", "false");
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY * 5);
});
expect(client.sendListFiles).toHaveBeenCalledTimes(1);
rerender(
,
);
client.sendListFiles.mockResolvedValue({
root: "/workspace",
files: [file("data/report.csv")],
});
rerender(
,
);
await act(async () => {
await vi.advanceTimersByTimeAsync(HOVER_EXPAND_DELAY);
});
expect(client.sendListFiles).toHaveBeenCalledTimes(2);
expect(folder).toHaveAttribute("aria-expanded", "true");
} finally {
vi.useRealTimers();
}
});
it("keeps partial results when a root fails, and retries all roots", async () => {
client.getFileRoots.mockResolvedValue({
roots: [
{ path: "/workspace", name: "workspace", isPrimary: true },
{ path: "/shared", name: "shared", isPrimary: false },
],
});
let sharedUnavailable = true;
client.sendSearchFiles.mockImplementation(async ({ path, query }) => {
if (path === "/shared" && sharedUnavailable) {
throw new Error("Storage unavailable");
}
return {
files: [{ ...file("report.txt"), path: `${path}/report.txt` }],
query,
totalFound: 1,
};
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
expect(await screen.findByRole("alert")).toHaveTextContent(
"Some locations could not be searched",
);
expect(
screen.getByRole("treeitem", { name: "/workspace/report.txt" }),
).toBeInTheDocument();
fireEvent.click(
screen.getByRole("treeitem", { name: "/workspace/report.txt" }),
);
const retry = screen.getByRole("button", { name: "Retry" });
retry.focus();
sharedUnavailable = false;
expect(fireEvent.keyDown(retry, { key: "Enter" })).toBe(true);
// jsdom does not synthesize a button click from Enter.
fireEvent.click(retry);
await screen.findByText("2 matches");
expect(screen.queryByText("Preview: report.txt")).not.toBeInTheDocument();
expect(
client.sendSearchFiles.mock.calls.map(([request]) => request.path),
).toEqual(["/workspace", "/shared", "/workspace", "/shared"]);
});
it.each([
{ counts: [100, 100], truncated: false },
{ counts: [200, 0], truncated: true },
{ counts: [150, 100], truncated: true },
])(
"reports search truncation correctly for $counts",
async ({ counts, truncated }) => {
client.getFileRoots.mockResolvedValue({
roots: [
{ path: "/workspace", name: "workspace", isPrimary: true },
{ path: "/shared", name: "shared", isPrimary: false },
],
});
client.sendSearchFiles.mockImplementation(async ({ path, query }) => {
const count = counts[path === "/workspace" ? 0 : 1];
const files = Array.from({ length: count }, (_, index) => ({
...file(`report-${index}`),
path: `${path}/report-${index}`,
}));
return { files, query, totalFound: files.length };
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
const status = await screen.findByText(/200 matches/);
expect(status.textContent?.includes("Refine your search")).toBe(
truncated,
);
},
);
it("keeps keyboard navigation aligned with a clicked row", async () => {
client.sendListFiles.mockResolvedValue({
root: "/workspace",
files: [file("a.txt"), file("b.txt"), file("c.txt")],
});
render(, { wrapper });
const middle = await screen.findByRole("treeitem", { name: "b.txt" });
fireEvent.click(screen.getByText("b.txt"));
expect(middle).toHaveFocus();
fireEvent.keyDown(middle, { key: "ArrowDown" });
expect(screen.getByRole("treeitem", { name: "c.txt" })).toHaveFocus();
});
it("closes failed expansions without persisting them and allows retry", async () => {
render(, { wrapper });
const folder = await screen.findByRole("treeitem", { name: "data" });
client.sendListFiles.mockRejectedValueOnce(new Error("Offline"));
fireEvent.click(folder);
fireEvent.keyDown(folder, { key: "ArrowRight" });
await waitFor(() =>
expect(folder).toHaveAttribute("aria-expanded", "false"),
);
expect(
store.get(openStateAtom)[fileTreeNodeId("/workspace", "/workspace/data")],
).toBe(false);
fireEvent.keyDown(folder, { key: "ArrowRight" });
expect(
await screen.findByRole("treeitem", { name: "report.csv" }),
).toBeVisible();
});
it("focuses a clicked search result so Enter opens that result", async () => {
client.sendSearchFiles.mockResolvedValue({
files: [file("report-first.txt"), file("report-second.txt")],
query: "report",
totalFound: 2,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
input.focus();
fireEvent.change(input, { target: { value: "report" } });
await screen.findByText("2 matches");
fireEvent.click(screen.getByText("report-second.txt"));
const second = screen.getByRole("treeitem", {
name: "/workspace/report-second.txt",
});
expect(second).toHaveFocus();
fireEvent.keyDown(second, { key: "Enter" });
expect(await screen.findByText("Preview: report-second.txt")).toBeVisible();
});
it("passes hidden-file visibility to search and refetches when it changes", async () => {
client.sendSearchFiles.mockResolvedValue({
files: [],
query: "note",
totalFound: 0,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "note" } });
await screen.findByText("No matching files or folders.");
expect(client.sendSearchFiles).toHaveBeenLastCalledWith(
expect.objectContaining({ includeHidden: true }),
);
fireEvent.click(screen.getByTestId("file-explorer-hidden-files-button"));
await waitFor(() =>
expect(client.sendSearchFiles).toHaveBeenLastCalledWith(
expect.objectContaining({ includeHidden: false }),
),
);
});
it("coalesces superseded searches without overlapping server scans", async () => {
const slow = new Deferred();
client.sendSearchFiles
.mockReturnValueOnce(slow.promise)
.mockResolvedValueOnce({
files: [file("notes.txt")],
query: "notes",
totalFound: 1,
});
render(, { wrapper });
const input = await screen.findByRole("textbox", {
name: "Search files and folders",
});
fireEvent.change(input, { target: { value: "report" } });
await waitFor(() => expect(client.sendSearchFiles).toHaveBeenCalledOnce());
fireEvent.change(input, { target: { value: "notes" } });
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 350));
});
expect(client.sendSearchFiles).toHaveBeenCalledOnce();
fireEvent.change(input, { target: { value: "note" } });
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 350));
});
expect(client.sendSearchFiles).toHaveBeenCalledOnce();
await act(async () => {
slow.resolve({
files: [file("data/report.csv")],
query: "report",
totalFound: 1,
});
});
await screen.findByText("1 match");
expect(client.sendSearchFiles).toHaveBeenCalledTimes(2);
expect(client.sendSearchFiles).toHaveBeenLastCalledWith(
expect.objectContaining({ query: "note" }),
);
expect(
screen.getByRole("treeitem", { name: "/workspace/notes.txt" }),
).toBeVisible();
expect(
screen.queryByRole("treeitem", { name: "/workspace/data/report.csv" }),
).not.toBeInTheDocument();
});
});