fix(file-tree): add missing path segments
This commit is contained in:
parent
28d5c05225
commit
a7c6808583
6 changed files with 174 additions and 4 deletions
|
|
@ -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(
|
||||
<FileTree
|
||||
files={[
|
||||
{ name: "subdir", path: "/project/subdir", type: "folder" },
|
||||
{ name: "file1", path: "/project/subdir/file1", type: "file" },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "project" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FileTree Selection Logic", () => {
|
||||
|
|
|
|||
77
app/client/components/__test__/restore-form.test.tsx
Normal file
77
app/client/components/__test__/restore-form.test.tsx
Normal file
|
|
@ -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(
|
||||
<RestoreForm
|
||||
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
|
||||
snapshotId="snap-1"
|
||||
returnPath="/repositories/repo-1/snap-1"
|
||||
queryBasePath="/mnt/project/subdir"
|
||||
displayBasePath="/mnt"
|
||||
/>,
|
||||
);
|
||||
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string> | 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { SnapshotFileBrowser } from "../snapshot-file-browser";
|
|||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("SnapshotFileBrowser", () => {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ const mockScheduleDetailsRequests = () => {
|
|||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("ScheduleDetailsPage", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue