diff --git a/app/client/components/__test__/file-tree.test.tsx b/app/client/components/__test__/file-tree.test.tsx
index 70016899..c01f71ca 100644
--- a/app/client/components/__test__/file-tree.test.tsx
+++ b/app/client/components/__test__/file-tree.test.tsx
@@ -1,6 +1,6 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */
-import { expect, test, describe } from "bun:test";
-import { render, screen, fireEvent, within } from "@testing-library/react";
+import { afterEach, expect, test, describe } from "bun:test";
+import { cleanup, render, screen, fireEvent, within } from "@testing-library/react";
import { useState } from "react";
import { FileTree, type FileEntry } from "../file-tree";
@@ -39,6 +39,10 @@ const getSelectedPaths = () => {
return JSON.parse(selectedPaths ?? "[]") as string[];
};
+afterEach(() => {
+ cleanup();
+});
+
describe("FileTree Pagination", () => {
const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
@@ -181,6 +185,19 @@ describe("FileTree Pagination", () => {
expect(screen.queryByText("Load more files")).toBeNull();
});
+
+ test("renders missing ancestor folders for nested paths", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("button", { name: "project" })).toBeTruthy();
+ });
});
describe("FileTree Selection Logic", () => {
diff --git a/app/client/components/__test__/restore-form.test.tsx b/app/client/components/__test__/restore-form.test.tsx
new file mode 100644
index 00000000..97dcfbf3
--- /dev/null
+++ b/app/client/components/__test__/restore-form.test.tsx
@@ -0,0 +1,77 @@
+import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
+import { HttpResponse, http, server } from "~/test/msw/server";
+import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
+import { fromAny } from "@total-typescript/shoehorn";
+
+await mock.module("@tanstack/react-router", () => ({
+ useNavigate: () => mock(() => {}),
+}));
+
+import { RestoreForm } from "../restore-form";
+
+class MockEventSource {
+ addEventListener() {}
+ close() {}
+ onerror: ((event: Event) => void) | null = null;
+
+ constructor(public url: string) {}
+}
+
+const originalEventSource = globalThis.EventSource;
+
+beforeEach(() => {
+ globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
+});
+
+afterEach(() => {
+ globalThis.EventSource = originalEventSource;
+ cleanup();
+});
+
+describe("RestoreForm", () => {
+ test("restores the selected ancestor folder path from a broader display root", async () => {
+ let restoreRequestBody: unknown;
+
+ server.use(
+ http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", () => {
+ return HttpResponse.json({
+ files: [
+ { name: "subdir", path: "/mnt/project/subdir", type: "dir" },
+ { name: "deep.tx", path: "/mnt/project/subdir/deep.tx", type: "file" },
+ ],
+ });
+ }),
+ http.post("/api/v1/repositories/:shortId/restore", async ({ request }) => {
+ restoreRequestBody = await request.json();
+ return HttpResponse.json({
+ success: true,
+ message: "Snapshot restored successfully",
+ filesRestored: 1,
+ filesSkipped: 0,
+ });
+ }),
+ );
+
+ render(
+ ,
+ );
+
+ const row = await screen.findByRole("button", { name: "project" });
+ await userEvent.click(within(row).getByRole("checkbox"));
+ await userEvent.click(screen.getByRole("button", { name: "Restore 1 item" }));
+
+ await waitFor(() => {
+ expect(restoreRequestBody).toEqual({
+ snapshotId: "snap-1",
+ include: ["/mnt/project"],
+ overwrite: "always",
+ });
+ });
+ });
+});
diff --git a/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx b/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx
index 568461eb..463f6bc0 100644
--- a/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx
+++ b/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx
@@ -20,7 +20,7 @@ const snapshotFiles = {
import { SnapshotTreeBrowser } from "../snapshot-tree-browser";
-const mockListSnapshotFiles = () => {
+const mockListSnapshotFiles = (response = snapshotFiles) => {
const requests: SnapshotFilesRequest[] = [];
server.use(
@@ -34,7 +34,7 @@ const mockListSnapshotFiles = () => {
limit: url.searchParams.get("limit"),
});
- return HttpResponse.json(snapshotFiles);
+ return HttpResponse.json(response);
}),
);
@@ -66,6 +66,49 @@ describe("SnapshotTreeBrowser", () => {
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
});
+ test("renders ancestor folders when the query root is nested multiple levels below the display root", async () => {
+ mockListSnapshotFiles({
+ files: [
+ { name: "subdir", path: "/mnt/project/subdir", type: "dir" },
+ { name: "a.txt", path: "/mnt/project/subdir/a.txt", type: "file" },
+ ],
+ });
+
+ renderSnapshotTreeBrowser({
+ queryBasePath: "/mnt/project/subdir",
+ displayBasePath: "/mnt",
+ });
+
+ expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
+ });
+
+ test("returns the ancestor folder path when selecting above the query root", async () => {
+ mockListSnapshotFiles({
+ files: [
+ { name: "subdir", path: "/mnt/project/subdir", type: "dir" },
+ { name: "a.txt", path: "/mnt/project/subdir/a.txt", type: "file" },
+ ],
+ });
+
+ let selectedPaths: Set | undefined;
+
+ renderSnapshotTreeBrowser({
+ queryBasePath: "/mnt/project/subdir",
+ displayBasePath: "/mnt",
+ withCheckboxes: true,
+ onSelectionChange: (paths) => {
+ selectedPaths = paths;
+ },
+ });
+
+ const row = await screen.findByRole("button", { name: "project" });
+ const checkbox = within(row).getByRole("checkbox");
+
+ await userEvent.click(checkbox);
+
+ expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]);
+ });
+
test("shows selected folder state when full paths are provided from the parent", async () => {
mockListSnapshotFiles();
diff --git a/app/client/components/file-tree.tsx b/app/client/components/file-tree.tsx
index ca543893..47763725 100644
--- a/app/client/components/file-tree.tsx
+++ b/app/client/components/file-tree.tsx
@@ -627,6 +627,10 @@ function buildFileList(files: FileEntry[], foldersOnly = false): Node[] {
const depth = segments.length - 1;
const name = segments[segments.length - 1];
+ if (!name) {
+ continue;
+ }
+
if (!fileMap.has(file.path)) {
const isFile = file.type === "file";
fileMap.set(file.path, {
@@ -638,6 +642,33 @@ function buildFileList(files: FileEntry[], foldersOnly = false): Node[] {
size: file.size,
});
}
+
+ let parentPath = file.path;
+ while (true) {
+ const lastSlashIndex = parentPath.lastIndexOf("/");
+ if (lastSlashIndex <= 0) {
+ break;
+ }
+
+ parentPath = parentPath.slice(0, lastSlashIndex);
+ if (fileMap.has(parentPath)) {
+ continue;
+ }
+
+ const parentSegments = parentPath.split("/").filter((segment) => segment);
+ const parentName = parentSegments[parentSegments.length - 1];
+ if (!parentName) {
+ continue;
+ }
+
+ fileMap.set(parentPath, {
+ kind: "folder",
+ id: fileMap.size,
+ name: parentName,
+ fullPath: parentPath,
+ depth: parentSegments.length - 1,
+ });
+ }
}
// Convert map to array and sort
diff --git a/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx b/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx
index 2fe14a0c..7940a2fc 100644
--- a/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx
+++ b/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx
@@ -23,6 +23,7 @@ import { SnapshotFileBrowser } from "../snapshot-file-browser";
afterEach(() => {
cleanup();
+ mock.restore();
});
describe("SnapshotFileBrowser", () => {
diff --git a/app/client/modules/backups/routes/__tests__/backup-details.test.tsx b/app/client/modules/backups/routes/__tests__/backup-details.test.tsx
index 7111f932..97d8c6f6 100644
--- a/app/client/modules/backups/routes/__tests__/backup-details.test.tsx
+++ b/app/client/modules/backups/routes/__tests__/backup-details.test.tsx
@@ -78,6 +78,7 @@ const mockScheduleDetailsRequests = () => {
afterEach(() => {
cleanup();
+ mock.restore();
});
describe("ScheduleDetailsPage", () => {