test: simplify file-tree selection & login mocking

This commit is contained in:
Nicolas Meienberger 2026-03-22 12:06:48 +01:00
parent 4fec2777ce
commit c93b076bf6
2 changed files with 60 additions and 127 deletions

View file

@ -1,8 +1,44 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */ /** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */
import { expect, test, describe } from "bun:test"; import { expect, test, describe } from "bun:test";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent, within } from "@testing-library/react";
import { useState } from "react";
import { FileTree, type FileEntry } from "../file-tree"; import { FileTree, type FileEntry } from "../file-tree";
const getCheckboxFor = (name: string) => {
const row = screen.getByRole("button", { name });
return within(row).getByRole("checkbox");
};
const FileTreeSelection = ({
files,
initialSelectedPaths = [],
expandedFolders,
}: {
files: FileEntry[];
initialSelectedPaths?: string[];
expandedFolders?: Set<string>;
}) => {
const [selectedPaths, setSelectedPaths] = useState(() => new Set(initialSelectedPaths));
return (
<>
<FileTree
files={files}
withCheckboxes={true}
selectedPaths={selectedPaths}
onSelectionChange={setSelectedPaths}
expandedFolders={expandedFolders}
/>
<output aria-label="Selected paths">{JSON.stringify(Array.from(selectedPaths).sort())}</output>
</>
);
};
const getSelectedPaths = () => {
const selectedPaths = screen.getByLabelText("Selected paths").textContent;
return JSON.parse(selectedPaths ?? "[]") as string[];
};
describe("FileTree Pagination", () => { describe("FileTree Pagination", () => {
const testFiles: FileEntry[] = [ const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" }, { name: "root", path: "/root", type: "folder" },
@ -157,154 +193,67 @@ describe("FileTree Selection Logic", () => {
{ name: "upload", path: "/root/photos/upload", type: "folder" }, { name: "upload", path: "/root/photos/upload", type: "folder" },
]; ];
test("selecting a folder simplifies to parent if it's the only child", async () => { test("selecting a folder simplifies to parent if it's the only child", () => {
let currentSelection = new Set<string>(); render(<FileTreeSelection files={testFiles} expandedFolders={new Set(testFiles.map((f) => f.path))} />);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render( fireEvent.click(getCheckboxFor("photos"));
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
const photosCheckbox = screen.getByText("photos").parentElement?.querySelector('button[role="checkbox"]'); expect(getSelectedPaths()).toEqual(["/root"]);
expect(photosCheckbox).toBeTruthy();
fireEvent.click(photosCheckbox!);
expect(currentSelection.has("/root")).toBe(true);
expect(currentSelection.size).toBe(1);
}); });
test("unselecting a child removes the parent from selection", async () => { test("unselecting a child removes the parent from selection", () => {
let currentSelection = new Set<string>(["/root"]);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render( render(
<FileTree <FileTreeSelection
files={testFiles} files={testFiles}
withCheckboxes={true} initialSelectedPaths={["/root"]}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))} expandedFolders={new Set(testFiles.map((f) => f.path))}
/>, />,
); );
const libraryCheckbox = screen.getByText("library").parentElement?.querySelector('button[role="checkbox"]'); fireEvent.click(getCheckboxFor("library"));
fireEvent.click(libraryCheckbox!);
expect(currentSelection.has("/root")).toBe(false); expect(getSelectedPaths()).toEqual(["/root/photos/backups", "/root/photos/profile", "/root/photos/upload"]);
expect(currentSelection.has("/root/photos")).toBe(false);
expect(currentSelection.has("/root/photos/backups")).toBe(true);
expect(currentSelection.has("/root/photos/profile")).toBe(true);
expect(currentSelection.has("/root/photos/upload")).toBe(true);
expect(currentSelection.size).toBe(3);
}); });
test("recursive simplification when all children are selected", async () => { test("recursive simplification when all children are selected", () => {
let currentSelection = new Set<string>(); render(<FileTreeSelection files={testFiles} expandedFolders={new Set(testFiles.map((f) => f.path))} />);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
const { rerender } = render(
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
const children = ["backups", "library", "profile", "upload"]; const children = ["backups", "library", "profile", "upload"];
for (const name of children) { for (const name of children) {
const checkbox = screen.getByText(name).parentElement?.querySelector('button[role="checkbox"]'); fireEvent.click(getCheckboxFor(name));
fireEvent.click(checkbox!);
rerender(
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
} }
expect(currentSelection.has("/root")).toBe(true); expect(getSelectedPaths()).toEqual(["/root"]);
expect(currentSelection.size).toBe(1);
}); });
test("does not simplify to parent if not all children are selected", async () => { test("does not simplify to parent if not all children are selected", () => {
const multipleFiles: FileEntry[] = [ const multipleFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" }, { name: "root", path: "/root", type: "folder" },
{ name: "child1", path: "/root/child1", type: "folder" }, { name: "child1", path: "/root/child1", type: "folder" },
{ name: "child2", path: "/root/child2", type: "folder" }, { name: "child2", path: "/root/child2", type: "folder" },
]; ];
let currentSelection = new Set<string>(); render(<FileTreeSelection files={multipleFiles} expandedFolders={new Set(multipleFiles.map((f) => f.path))} />);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render( fireEvent.click(getCheckboxFor("child1"));
<FileTree
files={multipleFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(multipleFiles.map((f) => f.path))}
/>,
);
const child1Checkbox = screen.getByText("child1").parentElement?.querySelector('button[role="checkbox"]'); expect(getSelectedPaths()).toEqual(["/root/child1"]);
fireEvent.click(child1Checkbox!);
expect(currentSelection.has("/root/child1")).toBe(true);
expect(currentSelection.has("/root")).toBe(false);
expect(currentSelection.size).toBe(1);
}); });
test("simplifies existing deep paths when parent is selected", async () => { test("simplifies existing deep paths when parent is selected", () => {
const files: FileEntry[] = [ const files: FileEntry[] = [
{ name: "hello", path: "/hello", type: "folder" }, { name: "hello", path: "/hello", type: "folder" },
{ name: "hello_prev", path: "/hello_prev", type: "folder" }, { name: "hello_prev", path: "/hello_prev", type: "folder" },
{ name: "service", path: "/service", type: "folder" }, { name: "service", path: "/service", type: "folder" },
]; ];
let currentSelection = new Set<string>(["/hello", "/hello_prev", "/service/app/data/upload"]);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render( render(
<FileTree <FileTreeSelection files={files} initialSelectedPaths={["/hello", "/hello_prev", "/service/app/data/upload"]} />,
files={files}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
/>,
); );
const serviceCheckbox = screen.getByText("service").parentElement?.querySelector('button[role="checkbox"]'); fireEvent.click(getCheckboxFor("service"));
expect(serviceCheckbox).toBeTruthy();
fireEvent.click(serviceCheckbox!); expect(getSelectedPaths()).toEqual(["/hello", "/hello_prev", "/service"]);
expect(currentSelection.has("/service")).toBe(true);
expect(currentSelection.has("/service/app/data/upload")).toBe(false);
expect(currentSelection.size).toBe(3); // /hello, /hello_prev, /service
}); });
}); });

View file

@ -6,24 +6,8 @@ await mock.module("@tanstack/react-router", () => ({
useNavigate: () => mock(() => {}), useNavigate: () => mock(() => {}),
})); }));
await mock.module("~/client/api-client/@tanstack/react-query.gen", () => ({ await mock.module("~/client/modules/sso/components/sso-login-section", () => ({
getPublicSsoProvidersOptions: () => ({ SsoLoginSection: () => null,
queryKey: ["public-sso-providers"],
queryFn: async () => ({ providers: [] }),
}),
}));
await mock.module("~/client/lib/auth-client", () => ({
authClient: {
getSession: mock(async () => ({ data: null })),
signIn: {
username: mock(async () => ({ data: null, error: null })),
sso: mock(async () => ({ data: null, error: null })),
},
twoFactor: {
verifyTotp: mock(async () => ({ data: null, error: null })),
},
},
})); }));
import { LoginPage } from "../login"; import { LoginPage } from "../login";