1e636fdbe2
Three reusable-widget follow-up fixes:
1. Reference sort_order independently reorderable. Reordering a
referenced widget now updates the widget_references.sort_order (per-
dashboard), not the shared widget instance sort_order. New backend
update_widget_reference method + PUT /api/widgets/references/{id}
endpoint. Frontend moveInstance checks _ref_id to choose the right
mutation (updateRef for references, saveWidget for owned).
2. Detach preserves service_id. detach_widget_reference now copies the
original widget's service_id into the clone, so service-bound widgets
(Grafana chart, Jellyfin activity) continue to render after detach.
3. Named dashboards support widget references. NamedDashboardPage
fetches useWidgetReferences('named:<slug>') and renders them via
WidgetInstanceCard alongside pinned links. 'Edit widgets' button
opens WidgetConfigDialog with dashboardScope='named:<slug>'.
Also: removed useMemo on combinedWidgets in WidgetConfigDialog to fix
a react-hooks/preserve-manual-memoization lint error (the React Compiler
ESLint plugin couldn't verify the spread+sort memoization).
283 backend tests pass (+1 update_reference test); 128 frontend tests
pass; ruff clean; 0 lint errors.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { WidgetConfigDialog } from "../WidgetConfigDialog";
|
|
|
|
// jsdom has no window.matchMedia; default to desktop (matches: false).
|
|
function setMatchMedia(matches: boolean) {
|
|
window.matchMedia = ((query: string) => ({
|
|
matches: query.includes("768") ? matches : false,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
addListener: () => {},
|
|
removeListener: () => {},
|
|
dispatchEvent: () => false,
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
|
|
vi.mock("../../hooks/useWidgets", () => ({
|
|
useWidgetInstances: () => ({ data: [] }),
|
|
useWidgetReferences: () => ({ data: [] }),
|
|
useSaveWidgetInstance: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
|
useDeleteWidgetInstance: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
|
useCreateWidgetReference: () => ({ mutateAsync: vi.fn() }),
|
|
useDeleteWidgetReference: () => ({ mutateAsync: vi.fn() }),
|
|
useDetachWidgetReference: () => ({ mutateAsync: vi.fn() }),
|
|
useUpdateWidgetReference: () => ({ mutateAsync: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useServices", () => ({
|
|
useServiceInstances: () => ({ data: [] }),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useSettings", () => ({
|
|
useTasks: () => ({ data: [] }),
|
|
}));
|
|
|
|
beforeEach(() => setMatchMedia(false));
|
|
|
|
describe("WidgetConfigDialog (desktop)", () => {
|
|
it("renders a Dialog with the dashboard widgets title at md+", () => {
|
|
render(<WidgetConfigDialog open={true} onClose={() => {}} />);
|
|
expect(
|
|
screen.getByRole("heading", { name: "Dashboard widgets" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("WidgetConfigDialog (mobile SheetForm — slice 8)", () => {
|
|
beforeEach(() => setMatchMedia(true));
|
|
|
|
it("renders a SheetForm with the dashboard widgets title below md", () => {
|
|
render(<WidgetConfigDialog open={true} onClose={() => {}} />);
|
|
expect(screen.getByText("Dashboard widgets")).toBeInTheDocument();
|
|
// List mode footer: "Done" button closes.
|
|
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("prompts before discarding a widget draft (R4.5)", async () => {
|
|
const { userEvent } = await import("@testing-library/user-event");
|
|
render(<WidgetConfigDialog open={true} onClose={() => {}} />);
|
|
|
|
// Enter draft mode by clicking an "Add widget" button.
|
|
await userEvent.click(screen.getByRole("button", { name: /Backups/i }));
|
|
|
|
// Now in draft mode — Cancel should prompt before resetting.
|
|
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
expect(
|
|
screen.getByRole("heading", { name: "Discard changes?" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|