feat(frontend): slice 4 — migrate Dashboard + Applications to shadcn/Tailwind
Web UI rework. - Migrate pages/Dashboard.tsx off @mui (shortcut CRUD dialogs, machine switcher, BackupDashboardWidget mount; shortcuts reuse ConfirmDialog/ DialogFooter from slice 2) - Migrate pages/Applications.tsx shell off @mui (tabs + library counts); keeps <Media/> child intact (Media.tsx still MUI, deferred to slice 7 with its DataGrid) - Behavioral tests for both pages Gate: build + lint + test green.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Applications } from "../Applications";
|
||||
|
||||
// Applications still embeds the MUI Media child (migrated in slice 7). Mock it
|
||||
// so this slice-4 test stays focused on the migrated Applications shell and
|
||||
// does not pull the still-MUI DataGrid into the jsdom render.
|
||||
vi.mock("../Media", () => ({
|
||||
Media: () => <div data-testid="media-child">Media</div>,
|
||||
}));
|
||||
|
||||
vi.mock("react-router-dom", () => ({
|
||||
useSearchParams: () => [new URLSearchParams(), vi.fn()],
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useSettings", () => ({
|
||||
useMonitoringSettings: () => ({
|
||||
data: [
|
||||
{
|
||||
id: "m1",
|
||||
name: "Main",
|
||||
enabled: true,
|
||||
services: ["jellyfin"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useDashboard", () => ({
|
||||
useCounts: () => ({
|
||||
data: { movies: 10, series: 5, episodes: 100 },
|
||||
}),
|
||||
useLibraries: () => ({
|
||||
data: [
|
||||
{ library: "Movies", total: 10, movies: 10, series: 0 },
|
||||
{ library: "Shows", total: 5, movies: 0, series: 5 },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Applications", () => {
|
||||
it("renders the Jellyfin library counts grid and tabs, and keeps the Media child", () => {
|
||||
render(<Applications />);
|
||||
|
||||
// Library stats header.
|
||||
expect(screen.getByText("Library stats")).toBeInTheDocument();
|
||||
|
||||
// Counts: Total = 10 + 5 + 100 = 115, plus the per-type counts.
|
||||
expect(screen.getByText("115")).toBeInTheDocument();
|
||||
expect(screen.getByText("Episodes")).toBeInTheDocument();
|
||||
|
||||
// Library rows render their per-library totals (unique strings).
|
||||
expect(
|
||||
screen.getByText(/Total 10 · Movies 10 · Series 0/),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Total 5 · Movies 0 · Series 5/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Tabs present.
|
||||
expect(screen.getByRole("tab", { name: "Jellyfin" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "Nextcloud" })).toBeInTheDocument();
|
||||
|
||||
// The still-MUI Media child is rendered unchanged inside the Jellyfin tab.
|
||||
expect(screen.getByTestId("media-child")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Dashboard } from "../Dashboard";
|
||||
import type { DashboardShortcut } from "../../types";
|
||||
|
||||
// Stub the composed widgets so the test exercises Dashboard's own behavior
|
||||
// (shortcut CRUD) without rendering the session panel or the backup query.
|
||||
vi.mock("../../components/NowPlaying", () => ({
|
||||
NowPlaying: () => <div data-testid="now-playing-stub" />,
|
||||
}));
|
||||
vi.mock("../../components/BackupDashboardWidget", () => ({
|
||||
default: () => <div data-testid="backup-widget-stub" />,
|
||||
}));
|
||||
|
||||
const navigate = vi.fn();
|
||||
vi.mock("react-router-dom", () => ({
|
||||
useNavigate: () => navigate,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useSettings", () => ({
|
||||
useMonitoringSettings: () => ({ data: [] }),
|
||||
}));
|
||||
|
||||
const saveShortcutMutate = vi.fn().mockResolvedValue({});
|
||||
const deleteShortcutMutate = vi.fn();
|
||||
|
||||
let shortcuts: DashboardShortcut[] = [];
|
||||
|
||||
vi.mock("../../hooks/useDashboard", () => ({
|
||||
useActivity: () => ({ data: undefined }),
|
||||
useDashboardShortcuts: () => ({ data: shortcuts }),
|
||||
useSaveDashboardShortcut: () => ({ mutateAsync: saveShortcutMutate }),
|
||||
useDeleteDashboardShortcut: () => ({ mutate: deleteShortcutMutate }),
|
||||
}));
|
||||
|
||||
function websiteShortcut(
|
||||
overrides: Partial<DashboardShortcut> = {},
|
||||
): DashboardShortcut {
|
||||
return {
|
||||
id: "s1",
|
||||
label: "Wiki",
|
||||
shortcut_type: "website",
|
||||
enabled: true,
|
||||
icon: "📚",
|
||||
url: "example.com",
|
||||
task_id: "",
|
||||
machine_id: "",
|
||||
user_id: "",
|
||||
notes: "Team wiki",
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
...overrides,
|
||||
} as DashboardShortcut;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
navigate.mockReset();
|
||||
saveShortcutMutate.mockClear();
|
||||
deleteShortcutMutate.mockClear();
|
||||
shortcuts = [];
|
||||
});
|
||||
|
||||
describe("Dashboard", () => {
|
||||
it("shows the empty-state alert when there are no shortcuts", () => {
|
||||
render(<Dashboard />);
|
||||
expect(screen.getByText(/No shortcuts yet/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add shortcut" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a shortcut card and deletes it via the confirm dialog", async () => {
|
||||
shortcuts = [websiteShortcut()];
|
||||
render(<Dashboard />);
|
||||
|
||||
expect(screen.getByText("Wiki")).toBeInTheDocument();
|
||||
|
||||
// Open the delete confirm.
|
||||
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
|
||||
expect(screen.getByText("Delete shortcut?")).toBeInTheDocument();
|
||||
|
||||
// Confirm deletion -> delete mutation fires with the shortcut id.
|
||||
const dialogs = screen.getAllByRole("button", { name: "Delete" });
|
||||
// The card "Delete" plus the confirm "Delete"; confirm is the last one.
|
||||
await userEvent.click(dialogs[dialogs.length - 1]);
|
||||
expect(deleteShortcutMutate).toHaveBeenCalledTimes(1);
|
||||
expect(deleteShortcutMutate).toHaveBeenCalledWith("s1");
|
||||
});
|
||||
|
||||
it("creates a shortcut via the dialog and saves it", async () => {
|
||||
render(<Dashboard />);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add shortcut" }));
|
||||
|
||||
// Edit dialog opens in "New shortcut" mode.
|
||||
expect(screen.getByText("New shortcut")).toBeInTheDocument();
|
||||
|
||||
// Fill the label and save.
|
||||
await userEvent.type(screen.getByLabelText("Label"), "Grafana");
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Save shortcut" }),
|
||||
);
|
||||
|
||||
expect(saveShortcutMutate).toHaveBeenCalledTimes(1);
|
||||
const saved = saveShortcutMutate.mock.calls[0][0];
|
||||
expect(saved.label).toBe("Grafana");
|
||||
expect(saved.shortcut_type).toBe("website");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user