fix: split display path and query base path

Closes #709
This commit is contained in:
Nicolas Meienberger 2026-03-26 18:55:34 +01:00
parent 6354705626
commit 2a1eaf29d0
12 changed files with 191 additions and 135 deletions

1
.gitignore vendored
View file

@ -43,3 +43,4 @@ qa-output
.worktrees/ .worktrees/
.turbo .turbo
.superset

View file

@ -0,0 +1,86 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
const snapshotFiles = {
files: [
{ name: "project", path: "/mnt/project", type: "dir" },
{ name: "a.txt", path: "/mnt/project/a.txt", type: "file" },
],
};
await mock.module("@tanstack/react-query", () => ({
useQuery: () => ({ data: snapshotFiles, isLoading: false, error: null }),
useQueryClient: () => ({
ensureQueryData: async () => snapshotFiles,
prefetchQuery: async () => undefined,
}),
}));
import { SnapshotTreeBrowser } from "../snapshot-tree-browser";
afterEach(() => {
cleanup();
});
describe("SnapshotTreeBrowser", () => {
test("renders the query root folder when display base path is broader than query base path", () => {
render(
<SnapshotTreeBrowser
repositoryId="repo-1"
snapshotId="snap-1"
queryBasePath="/mnt/project"
displayBasePath="/mnt"
/>,
);
screen.getByRole("button", { name: "project" });
});
test("shows selected folder state when full paths are provided from the parent", () => {
render(
<SnapshotTreeBrowser
repositoryId="repo-1"
snapshotId="snap-1"
queryBasePath="/mnt/project"
displayBasePath="/mnt"
withCheckboxes
selectedPaths={new Set(["/mnt/project"])}
onSelectionChange={() => {}}
/>,
);
const row = screen.getByRole("button", { name: "project" });
const checkbox = within(row).getByRole("checkbox");
expect(checkbox.getAttribute("aria-checked")).toBe("true");
});
test("returns the full snapshot path and kind when selecting a displayed folder", () => {
let selectedPaths: Set<string> | undefined;
let selectedKind: "file" | "dir" | null = null;
render(
<SnapshotTreeBrowser
repositoryId="repo-1"
snapshotId="snap-1"
queryBasePath="/mnt/project"
displayBasePath="/mnt"
withCheckboxes
onSelectionChange={(paths) => {
selectedPaths = paths;
}}
onSingleSelectionKindChange={(kind) => {
selectedKind = kind;
}}
/>,
);
const row = screen.getByRole("button", { name: "project" });
const checkbox = within(row).getByRole("checkbox");
fireEvent.click(checkbox);
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]);
expect(selectedKind === "dir").toBe(true);
});
});

View file

@ -7,92 +7,91 @@ import { parseError } from "~/client/lib/errors";
import { normalizeAbsolutePath } from "@zerobyte/core/utils"; import { normalizeAbsolutePath } from "@zerobyte/core/utils";
import { logger } from "~/client/lib/logger"; import { logger } from "~/client/lib/logger";
function createPathPrefixFns(basePath: string) {
return {
strip(path: string) {
if (basePath === "/") return path;
if (path === basePath) return "/";
if (path.startsWith(`${basePath}/`)) return path.slice(basePath.length);
return path;
},
add(displayPath: string) {
if (basePath === "/") return displayPath;
if (displayPath === "/") return basePath;
return `${basePath}${displayPath}`;
},
};
}
type SnapshotTreeBrowserProps = FileBrowserUiProps & { type SnapshotTreeBrowserProps = FileBrowserUiProps & {
repositoryId: string; repositoryId: string;
snapshotId: string; snapshotId: string;
basePath?: string; queryBasePath?: string;
displayBasePath?: string;
pageSize?: number; pageSize?: number;
enabled?: boolean; enabled?: boolean;
onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void; onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void;
}; };
export const SnapshotTreeBrowser = ({ export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => {
repositoryId, const {
snapshotId, repositoryId,
basePath = "/", snapshotId,
pageSize = 500, queryBasePath = "/",
enabled = true, displayBasePath,
...uiProps pageSize = 500,
}: SnapshotTreeBrowserProps) => { enabled = true,
...uiProps
} = props;
const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps; const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const normalizedBasePath = normalizeAbsolutePath(basePath); const normalizedQueryBasePath = normalizeAbsolutePath(queryBasePath);
const normalizedDisplayBasePath = normalizeAbsolutePath(displayBasePath ?? normalizedQueryBasePath);
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
...listSnapshotFilesOptions({ ...listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId }, path: { shortId: repositoryId, snapshotId },
query: { path: normalizedBasePath }, query: { path: normalizedQueryBasePath },
}), }),
enabled, enabled,
}); });
const stripBasePath = useCallback( const displayPathFns = useMemo(() => createPathPrefixFns(normalizedDisplayBasePath), [normalizedDisplayBasePath]);
(path: string): string => {
if (normalizedBasePath === "/") return path;
if (path === normalizedBasePath) return "/";
if (path.startsWith(`${normalizedBasePath}/`)) {
return path.slice(normalizedBasePath.length);
}
return path;
},
[normalizedBasePath],
);
const addBasePath = useCallback(
(displayPath: string): string => {
if (normalizedBasePath === "/") return displayPath;
if (displayPath === "/") return normalizedBasePath;
return `${normalizedBasePath}${displayPath}`;
},
[normalizedBasePath],
);
const displaySelectedPaths = useMemo(() => { const displaySelectedPaths = useMemo(() => {
if (!selectedPaths) return undefined; if (!selectedPaths) return undefined;
const displayPaths = new Set<string>(); const displayPaths = new Set<string>();
for (const fullPath of selectedPaths) { for (const fullPath of selectedPaths) {
displayPaths.add(stripBasePath(fullPath)); displayPaths.add(displayPathFns.strip(fullPath));
} }
return displayPaths; return displayPaths;
}, [selectedPaths, stripBasePath]); }, [displayPathFns, selectedPaths]);
const fileBrowser = useFileBrowser({ const fileBrowser = useFileBrowser({
initialData: data, initialData: data,
isLoading, isLoading,
fetchFolder: async (path, offset = 0) => { fetchFolder: async (displayPath, offset = 0) => {
return await queryClient.ensureQueryData( return await queryClient.ensureQueryData(
listSnapshotFilesOptions({ listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId }, path: { shortId: repositoryId, snapshotId },
query: { path, offset: offset, limit: pageSize }, query: { path: displayPath, offset: offset, limit: pageSize },
}), }),
); );
}, },
prefetchFolder: (path) => { prefetchFolder: (displayPath) => {
void queryClient void queryClient
.prefetchQuery( .prefetchQuery(
listSnapshotFilesOptions({ listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId }, path: { shortId: repositoryId, snapshotId },
query: { path, offset: 0, limit: pageSize }, query: { path: displayPath, offset: 0, limit: pageSize },
}), }),
) )
.catch((e) => logger.error(e)); .catch((e) => logger.error(e));
}, },
pathTransform: { pathTransform: displayPathFns,
strip: stripBasePath,
add: addBasePath,
},
}); });
const displayPathKinds = useMemo(() => { const displayPathKinds = useMemo(() => {
@ -109,7 +108,7 @@ export const SnapshotTreeBrowser = ({
const nextFullPaths = new Set<string>(); const nextFullPaths = new Set<string>();
for (const displayPath of nextDisplayPaths) { for (const displayPath of nextDisplayPaths) {
nextFullPaths.add(addBasePath(displayPath)); nextFullPaths.add(displayPathFns.add(displayPath));
} }
if (onSingleSelectionKindChange) { if (onSingleSelectionKindChange) {
@ -127,7 +126,7 @@ export const SnapshotTreeBrowser = ({
onSelectionChange(nextFullPaths); onSelectionChange(nextFullPaths);
}, },
[onSelectionChange, addBasePath, onSingleSelectionKindChange, displayPathKinds], [displayPathFns, displayPathKinds, onSelectionChange, onSingleSelectionKindChange],
); );
return ( return (

View file

@ -33,14 +33,15 @@ interface RestoreFormProps {
repository: Repository; repository: Repository;
snapshotId: string; snapshotId: string;
returnPath: string; returnPath: string;
basePath?: string; queryBasePath?: string;
displayBasePath?: string;
} }
export function RestoreForm({ repository, snapshotId, returnPath, basePath }: RestoreFormProps) { export function RestoreForm({ repository, snapshotId, returnPath, queryBasePath, displayBasePath }: RestoreFormProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { addEventListener } = useServerEvents(); const { addEventListener } = useServerEvents();
const volumeBasePath = basePath ?? "/"; const snapshotBasePath = queryBasePath ?? "/";
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>("original"); const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>("original");
const [customTargetPath, setCustomTargetPath] = useState(""); const [customTargetPath, setCustomTargetPath] = useState("");
@ -346,7 +347,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
<SnapshotTreeBrowser <SnapshotTreeBrowser
repositoryId={repository.shortId} repositoryId={repository.shortId}
snapshotId={snapshotId} snapshotId={snapshotId}
basePath={volumeBasePath} queryBasePath={snapshotBasePath}
displayBasePath={displayBasePath}
pageSize={500} pageSize={500}
className="flex flex-1 min-h-0 flex-col" className="flex flex-1 min-h-0 flex-col"
treeContainerClassName="overflow-auto flex-1 min-h-0 border border-border rounded-md bg-card m-4" treeContainerClassName="overflow-auto flex-1 min-h-0 border border-border rounded-md bg-card m-4"

View file

@ -1,5 +1,4 @@
import { RotateCcw, Trash2 } from "lucide-react"; import { RotateCcw, Trash2 } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Button, buttonVariants } from "~/client/components/ui/button"; import { Button, buttonVariants } from "~/client/components/ui/button";
import type { Snapshot } from "~/client/lib/types"; import type { Snapshot } from "~/client/lib/types";
@ -7,14 +6,13 @@ import { useTimeFormat } from "~/client/lib/datetime";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser"; import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser";
import { getBackupScheduleOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { findCommonAncestor } from "@zerobyte/core/utils";
import { getVolumeMountPath } from "~/client/lib/volume-path";
interface Props { interface Props {
snapshot: Snapshot; snapshot: Snapshot;
repositoryId: string; repositoryId: string;
backupId?: string; backupId?: string;
basePath?: string; displayBasePath?: string;
onDeleteSnapshot?: (snapshotId: string) => void; onDeleteSnapshot?: (snapshotId: string) => void;
isDeletingSnapshot?: boolean; isDeletingSnapshot?: boolean;
} }
@ -28,43 +26,11 @@ const treeProps = {
stateClassName: "flex-1 min-h-0", stateClassName: "flex-1 min-h-0",
} as const; } as const;
interface ScheduleAwareTreeBrowserProps {
scheduleShortId: string;
repositoryId: string;
snapshotId: string;
}
const ScheduleAwareTreeBrowser = ({ scheduleShortId, repositoryId, snapshotId }: ScheduleAwareTreeBrowserProps) => {
const { data: schedule, isPending } = useQuery({
...getBackupScheduleOptions({ path: { shortId: scheduleShortId } }),
retry: false,
});
if (isPending) {
return <TreeBrowserFallback />;
}
return (
<SnapshotTreeBrowser
repositoryId={repositoryId}
snapshotId={snapshotId}
basePath={schedule ? getVolumeMountPath(schedule.volume) : "/"}
{...treeProps}
/>
);
};
const TreeBrowserFallback = () => (
<div className={cn(treeProps.treeContainerClassName, "flex items-center justify-center")}>
<p className="text-muted-foreground">Loading volume info...</p>
</div>
);
export const SnapshotFileBrowser = (props: Props) => { export const SnapshotFileBrowser = (props: Props) => {
const { snapshot, repositoryId, backupId, basePath, onDeleteSnapshot, isDeletingSnapshot } = props; const { snapshot, repositoryId, backupId, displayBasePath, onDeleteSnapshot, isDeletingSnapshot } = props;
const { formatDateTime } = useTimeFormat(); const { formatDateTime } = useTimeFormat();
const scheduleShortId = !basePath ? backupId || snapshot.tags?.[0] : undefined; const queryBasePath = findCommonAncestor(snapshot.paths);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -110,27 +76,13 @@ export const SnapshotFileBrowser = (props: Props) => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex-1 overflow-hidden flex flex-col p-0"> <CardContent className="flex-1 overflow-hidden flex flex-col p-0">
{basePath ? ( <SnapshotTreeBrowser
<SnapshotTreeBrowser repositoryId={repositoryId}
repositoryId={repositoryId} snapshotId={snapshot.short_id}
snapshotId={snapshot.short_id} queryBasePath={queryBasePath}
basePath={basePath} displayBasePath={displayBasePath}
{...treeProps} {...treeProps}
/> />
) : scheduleShortId ? (
<ScheduleAwareTreeBrowser
scheduleShortId={scheduleShortId}
repositoryId={repositoryId}
snapshotId={snapshot.short_id}
/>
) : (
<SnapshotTreeBrowser
repositoryId={repositoryId}
snapshotId={snapshot.short_id}
basePath="/"
{...treeProps}
/>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -34,6 +34,7 @@ import { ScheduleNotificationsConfig } from "../components/schedule-notification
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
import { BackupSummaryCard } from "~/client/components/backup-summary-card"; import { BackupSummaryCard } from "~/client/components/backup-summary-card";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { getVolumeMountPath } from "~/client/lib/volume-path";
import type { import type {
BackupSchedule, BackupSchedule,
NotificationDestination, NotificationDestination,
@ -303,6 +304,7 @@ export function ScheduleDetailsPage(props: Props) {
snapshot={selectedSnapshot} snapshot={selectedSnapshot}
repositoryId={schedule.repository.shortId} repositoryId={schedule.repository.shortId}
backupId={schedule.shortId} backupId={schedule.shortId}
displayBasePath={getVolumeMountPath(schedule.volume)}
onDeleteSnapshot={handleDeleteSnapshot} onDeleteSnapshot={handleDeleteSnapshot}
isDeletingSnapshot={deleteSnapshot.isPending} isDeletingSnapshot={deleteSnapshot.isPending}
/> />

View file

@ -5,11 +5,20 @@ type Props = {
repository: Repository; repository: Repository;
snapshotId: string; snapshotId: string;
returnPath: string; returnPath: string;
basePath?: string; queryBasePath?: string;
displayBasePath?: string;
}; };
export function RestoreSnapshotPage(props: Props) { export function RestoreSnapshotPage(props: Props) {
const { returnPath, snapshotId, repository, basePath } = props; const { returnPath, snapshotId, repository, queryBasePath, displayBasePath } = props;
return <RestoreForm repository={repository} snapshotId={snapshotId} returnPath={returnPath} basePath={basePath} />; return (
<RestoreForm
repository={repository}
snapshotId={snapshotId}
returnPath={returnPath}
queryBasePath={queryBasePath}
displayBasePath={displayBasePath}
/>
);
} }

View file

@ -101,7 +101,7 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot
<SnapshotFileBrowser <SnapshotFileBrowser
repositoryId={repositoryId} repositoryId={repositoryId}
snapshot={data} snapshot={data}
basePath={backupSchedule ? getVolumeMountPath(backupSchedule.volume) : undefined} displayBasePath={backupSchedule ? getVolumeMountPath(backupSchedule.volume) : undefined}
/> />
) : ( ) : (
<SnapshotFileBrowserSkeleton /> <SnapshotFileBrowserSkeleton />

View file

@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot"; import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
import { getVolumeMountPath } from "~/client/lib/volume-path"; import { getVolumeMountPath } from "~/client/lib/volume-path";
import { findCommonAncestor } from "@zerobyte/core/utils";
export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({
component: RouteComponent, component: RouteComponent,
@ -29,7 +30,8 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
snapshot, snapshot,
repository, repository,
schedule: schedule.data, schedule: schedule.data,
basePath: getVolumeMountPath(schedule.data.volume), queryBasePath: findCommonAncestor(snapshot.paths),
displayBasePath: getVolumeMountPath(schedule.data.volume),
}; };
}, },
head: ({ params }) => ({ head: ({ params }) => ({
@ -53,14 +55,15 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
function RouteComponent() { function RouteComponent() {
const { backupId, snapshotId } = Route.useParams(); const { backupId, snapshotId } = Route.useParams();
const { repository, basePath } = Route.useLoaderData(); const { repository, queryBasePath, displayBasePath } = Route.useLoaderData();
return ( return (
<RestoreSnapshotPage <RestoreSnapshotPage
returnPath={`/backups/${backupId}`} returnPath={`/backups/${backupId}`}
snapshotId={snapshotId} snapshotId={snapshotId}
repository={repository} repository={repository}
basePath={basePath} queryBasePath={queryBasePath}
displayBasePath={displayBasePath}
/> />
); );
} }

View file

@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot"; import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
import { getVolumeMountPath } from "~/client/lib/volume-path"; import { getVolumeMountPath } from "~/client/lib/volume-path";
import { findCommonAncestor } from "@zerobyte/core/utils";
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({
component: RouteComponent, component: RouteComponent,
@ -15,16 +16,16 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { shortId: params.repositoryId } }) }), context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { shortId: params.repositoryId } }) }),
]); ]);
let basePath: string | undefined; let displayBasePath: string | undefined;
const scheduleShortId = snapshot.tags?.[0]; const scheduleShortId = snapshot.tags?.[0];
if (scheduleShortId) { if (scheduleShortId) {
const scheduleRes = await getBackupSchedule({ path: { shortId: scheduleShortId } }); const scheduleRes = await getBackupSchedule({ path: { shortId: scheduleShortId } });
if (scheduleRes.data) { if (scheduleRes.data) {
basePath = getVolumeMountPath(scheduleRes.data.volume); displayBasePath = getVolumeMountPath(scheduleRes.data.volume);
} }
} }
return { snapshot, repository, basePath }; return { snapshot, repository, queryBasePath: findCommonAncestor(snapshot.paths), displayBasePath };
}, },
staticData: { staticData: {
breadcrumb: (match) => [ breadcrumb: (match) => [
@ -47,14 +48,15 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
function RouteComponent() { function RouteComponent() {
const { repositoryId, snapshotId } = Route.useParams(); const { repositoryId, snapshotId } = Route.useParams();
const { repository, basePath } = Route.useLoaderData(); const { repository, queryBasePath, displayBasePath } = Route.useLoaderData();
return ( return (
<RestoreSnapshotPage <RestoreSnapshotPage
returnPath={`/repositories/${repositoryId}/${snapshotId}`} returnPath={`/repositories/${repositoryId}/${snapshotId}`}
repository={repository} repository={repository}
snapshotId={snapshotId} snapshotId={snapshotId}
basePath={basePath} queryBasePath={queryBasePath}
displayBasePath={displayBasePath}
/> />
); );
} }

View file

@ -35,7 +35,7 @@ function getRunId(testInfo: TestInfo) {
return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`; return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`;
} }
function getWorkerTestDataPath(runId: string) { function getWorkerTestDataPath() {
fs.mkdirSync(testDataPath, { recursive: true }); fs.mkdirSync(testDataPath, { recursive: true });
return testDataPath; return testDataPath;
} }
@ -49,7 +49,7 @@ function getScenarioNames(runId: string): ScenarioNames {
} }
function prepareTestFile(runId: string, fileName = "test.json"): string { function prepareTestFile(runId: string, fileName = "test.json"): string {
const runPath = path.join(getWorkerTestDataPath(runId), runId); const runPath = path.join(getWorkerTestDataPath(), runId);
fs.mkdirSync(runPath, { recursive: true }); fs.mkdirSync(runPath, { recursive: true });
const filePath = path.join(runPath, fileName); const filePath = path.join(runPath, fileName);
@ -58,8 +58,8 @@ function prepareTestFile(runId: string, fileName = "test.json"): string {
return filePath; return filePath;
} }
async function createBackupScenario(page: Page, names: ScenarioNames, runId: string, options: ScenarioOptions = {}) { async function createBackupScenario(page: Page, names: ScenarioNames, options: ScenarioOptions = {}) {
getWorkerTestDataPath(runId); getWorkerTestDataPath();
const volumeNameInput = page.getByRole("textbox", { name: "Name" }); const volumeNameInput = page.getByRole("textbox", { name: "Name" });
await expect(async () => { await expect(async () => {
@ -153,7 +153,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => {
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click(); await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("Backup started successfully")).toBeVisible();
@ -177,7 +177,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => {
test("can restore a single selected file to a custom location", async ({ page }, testInfo) => { test("can restore a single selected file to a custom location", async ({ page }, testInfo) => {
const runId = getRunId(testInfo); const runId = getRunId(testInfo);
const names = getScenarioNames(runId); const names = getScenarioNames(runId);
const workerTestDataPath = getWorkerTestDataPath(runId); const workerTestDataPath = getWorkerTestDataPath();
const fileName = `single-file-${runId}.json`; const fileName = `single-file-${runId}.json`;
const filePath = prepareTestFile(runId, fileName); const filePath = prepareTestFile(runId, fileName);
const restoreTargetPath = path.join(workerTestDataPath, fileName); const restoreTargetPath = path.join(workerTestDataPath, fileName);
@ -188,7 +188,7 @@ test("can restore a single selected file to a custom location", async ({ page },
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click(); await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("Backup started successfully")).toBeVisible();
@ -234,7 +234,7 @@ test("can re-tag a snapshot to another backup schedule", async ({ page }, testIn
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click(); await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("Backup started successfully")).toBeVisible();
@ -277,7 +277,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page },
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click(); await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("Backup started successfully")).toBeVisible();
@ -302,7 +302,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page },
test("can download a selected snapshot directory as a tar archive", async ({ page }, testInfo) => { test("can download a selected snapshot directory as a tar archive", async ({ page }, testInfo) => {
const runId = getRunId(testInfo); const runId = getRunId(testInfo);
const names = getScenarioNames(runId); const names = getScenarioNames(runId);
const workerTestDataPath = getWorkerTestDataPath(runId); const workerTestDataPath = getWorkerTestDataPath();
const fileName = `download-${runId}.json`; const fileName = `download-${runId}.json`;
const filePath = prepareTestFile(runId, fileName); const filePath = prepareTestFile(runId, fileName);
const downloadedPath = path.join(workerTestDataPath, `downloaded-${runId}.tar`); const downloadedPath = path.join(workerTestDataPath, `downloaded-${runId}.tar`);
@ -312,7 +312,7 @@ test("can download a selected snapshot directory as a tar archive", async ({ pag
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click(); await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("Backup started successfully")).toBeVisible();
@ -351,7 +351,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId); await createBackupScenario(page, names);
await gotoAndWaitForAppReady(page, "/backups"); await gotoAndWaitForAppReady(page, "/backups");
await page.getByText(names.backupName, { exact: true }).first().click(); await page.getByText(names.backupName, { exact: true }).first().click();
@ -378,7 +378,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page
test("backup respects include globs, exclusion patterns, and exclude-if-present", async ({ page }, testInfo) => { test("backup respects include globs, exclusion patterns, and exclude-if-present", async ({ page }, testInfo) => {
const runId = getRunId(testInfo); const runId = getRunId(testInfo);
const names = getScenarioNames(runId); const names = getScenarioNames(runId);
const workerTestDataPath = getWorkerTestDataPath(runId); const workerTestDataPath = getWorkerTestDataPath();
const keptDir = `kept-${runId}`; const keptDir = `kept-${runId}`;
const secondKeptDir = `second-kept-${runId}`; const secondKeptDir = `second-kept-${runId}`;
@ -433,7 +433,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present"
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId, { await createBackupScenario(page, names, {
includePatterns: [ includePatterns: [
`/${keptDir}`, `/${keptDir}`,
`/${secondKeptDir}`, `/${secondKeptDir}`,
@ -492,7 +492,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present"
test("backup can include a selected folder whose name contains brackets", async ({ page }, testInfo) => { test("backup can include a selected folder whose name contains brackets", async ({ page }, testInfo) => {
const runId = getRunId(testInfo); const runId = getRunId(testInfo);
const names = getScenarioNames(runId); const names = getScenarioNames(runId);
const workerTestDataPath = getWorkerTestDataPath(runId); const workerTestDataPath = getWorkerTestDataPath();
const bracketDir = `movies [${runId}]`; const bracketDir = `movies [${runId}]`;
const bracketPath = path.join(workerTestDataPath, bracketDir); const bracketPath = path.join(workerTestDataPath, bracketDir);
const fileName = `inside-${runId}.txt`; const fileName = `inside-${runId}.txt`;
@ -503,7 +503,7 @@ test("backup can include a selected folder whose name contains brackets", async
await gotoAndWaitForAppReady(page, "/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
await createBackupScenario(page, names, runId, { await createBackupScenario(page, names, {
selectedPaths: [`/${bracketDir}`], selectedPaths: [`/${bracketDir}`],
}); });

View file

@ -1,5 +1,5 @@
{ {
"include": ["**/*"], "include": ["app/**/*"],
"compilerOptions": { "compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"], "lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"], "types": ["node", "vite/client"],