Test: 10 more widget components

This commit is contained in:
shamoon
2026-02-02 22:31:17 -08:00
parent 6bdee5bade
commit cd6634e40a
11 changed files with 671 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/booklore/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "booklore" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("booklore.libraries")).toBeInTheDocument();
expect(screen.getByText("booklore.books")).toBeInTheDocument();
expect(screen.getByText("booklore.reading")).toBeInTheDocument();
expect(screen.getByText("booklore.finished")).toBeInTheDocument();
});
it("renders values with nullish fallback defaults", () => {
useWidgetAPI.mockReturnValue({
data: { libraries: 1, books: 2, finished: 4 }, // reading missing -> 0
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "booklore" } }} />, { settings: { hideErrors: false } });
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("0")).toBeInTheDocument();
expect(screen.getByText("4")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,74 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/firefly/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "firefly" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(2);
expect(screen.getByText("firefly.networth")).toBeInTheDocument();
expect(screen.getByText("firefly.budget")).toBeInTheDocument();
});
it("renders error UI when either request errors", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: { message: "nope" } }) // summary
.mockReturnValueOnce({ data: undefined, error: undefined }); // budgets
renderWithProviders(<Component service={{ widget: { type: "firefly" } }} />, { settings: { hideErrors: false } });
// The widget uses a string error, which Error normalizes to { message }.
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
expect(screen.getByText("Failed to load Firefly account summary and budgets")).toBeInTheDocument();
});
it("renders net worth and budget summary", () => {
useWidgetAPI
.mockReturnValueOnce({
data: { "net-worth-in-EUR": { value_parsed: "100" } },
error: undefined,
})
.mockReturnValueOnce({
data: {
data: [
{
type: "available_budgets",
attributes: {
amount: "100",
currency_symbol: "$",
spent_in_budgets: [{ sum: "-10" }],
},
},
],
},
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "firefly" } }} />, { settings: { hideErrors: false } });
expect(screen.getByText("100")).toBeInTheDocument();
expect(screen.getByText("$ 10 / $ 100")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,65 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/jellystat/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("defaults invalid days to 30 and renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "jellystat", days: -1 } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(service.widget.days).toBe(30);
expect(useWidgetAPI).toHaveBeenCalledWith(service.widget, "getViewsByLibraryType", { days: 30 });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("jellystat.songs")).toBeInTheDocument();
expect(screen.getByText("jellystat.movies")).toBeInTheDocument();
expect(screen.getByText("jellystat.episodes")).toBeInTheDocument();
expect(screen.getByText("jellystat.other")).toBeInTheDocument();
});
it("renders error UI when widget API errors", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
renderWithProviders(<Component service={{ widget: { type: "jellystat", days: 7 } }} />, {
settings: { hideErrors: false },
});
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
});
it("renders values when loaded", () => {
useWidgetAPI.mockReturnValue({
data: { Audio: 1, Movie: 2, Series: 3, Other: 4 },
error: undefined,
});
const { container } = renderWithProviders(<Component service={{ widget: { type: "jellystat", days: 7 } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("4")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,83 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/nextcloud/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders default placeholders (no cpu/memory blocks when fields are unset)", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "nextcloud" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.queryByText("nextcloud.cpuload")).toBeNull();
expect(screen.queryByText("nextcloud.memoryusage")).toBeNull();
expect(screen.getByText("nextcloud.freespace")).toBeInTheDocument();
expect(screen.getByText("nextcloud.activeusers")).toBeInTheDocument();
expect(screen.getByText("nextcloud.numfiles")).toBeInTheDocument();
expect(screen.getByText("nextcloud.numshares")).toBeInTheDocument();
});
it("respects widget.fields and renders computed values", () => {
useWidgetAPI.mockReturnValue({
data: {
ocs: {
data: {
nextcloud: {
system: {
cpuload: [0.5],
mem_total: "100",
mem_free: "50",
freespace: 1024,
},
storage: { num_files: 1 },
shares: { num_shares: 2 },
},
activeUsers: { last24hours: 3 },
},
},
},
error: undefined,
});
// 4 fields triggers the legacy behavior where CPU + memory are shown;
// Container then filters to exactly these fields.
const service = {
widget: { type: "nextcloud", fields: ["cpuload", "memoryusage", "freespace", "activeusers"] },
};
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("nextcloud.cpuload")).toBeInTheDocument();
expect(screen.getByText("nextcloud.memoryusage")).toBeInTheDocument();
expect(screen.getByText("nextcloud.freespace")).toBeInTheDocument();
expect(screen.getByText("nextcloud.activeusers")).toBeInTheDocument();
expect(screen.queryByText("nextcloud.numfiles")).toBeNull();
expect(screen.queryByText("nextcloud.numshares")).toBeNull();
// Values: cpu load 0.5, memory usage 50, freespace 1024, active users 3.
expect(screen.getByText("0.5")).toBeInTheDocument();
expect(screen.getByText("50")).toBeInTheDocument();
expect(screen.getByText("1024")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,52 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/peanut/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "peanut" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("peanut.battery_charge")).toBeInTheDocument();
expect(screen.getByText("peanut.ups_load")).toBeInTheDocument();
expect(screen.getByText("peanut.ups_status")).toBeInTheDocument();
});
it("renders legacy field mapping and status translation", () => {
useWidgetAPI.mockReturnValue({
data: {
"battery.charge": 55,
"ups.load": 12,
"ups.status": "OL",
},
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "peanut" } }} />, { settings: { hideErrors: false } });
expect(screen.getByText("55")).toBeInTheDocument();
expect(screen.getByText("12")).toBeInTheDocument();
expect(screen.getByText("peanut.online")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,77 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/proxmoxbackupserver/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // datastore
.mockReturnValueOnce({ data: undefined, error: undefined }) // tasks
.mockReturnValueOnce({ data: undefined, error: undefined }); // host
const { container } = renderWithProviders(<Component service={{ widget: { type: "proxmoxbackupserver" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("proxmoxbackupserver.datastore_usage")).toBeInTheDocument();
expect(screen.getByText("proxmoxbackupserver.failed_tasks_24h")).toBeInTheDocument();
expect(screen.getByText("proxmoxbackupserver.cpu_usage")).toBeInTheDocument();
expect(screen.getByText("proxmoxbackupserver.memory_usage")).toBeInTheDocument();
});
it("renders error UI when any endpoint errors", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined })
.mockReturnValueOnce({ data: undefined, error: { message: "nope" } })
.mockReturnValueOnce({ data: undefined, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "proxmoxbackupserver" } }} />, {
settings: { hideErrors: false },
});
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
});
it("renders computed values and caps failed tasks at 99+", () => {
useWidgetAPI
.mockReturnValueOnce({
data: {
data: [
{ store: "ds1", used: 50, total: 100 },
{ store: "ds2", used: 25, total: 50 },
],
},
error: undefined,
})
.mockReturnValueOnce({ data: { total: 1000 }, error: undefined })
.mockReturnValueOnce({ data: { data: { cpu: 0.2, memory: { used: 1, total: 4 } } }, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "proxmoxbackupserver", datastore: "ds2" } }} />, {
settings: { hideErrors: false },
});
// datastore usage for ds2: 25/50*100 = 50
expect(screen.getByText("50")).toBeInTheDocument();
expect(screen.getByText("20")).toBeInTheDocument(); // cpu usage
expect(screen.getByText("25")).toBeInTheDocument(); // memory usage
expect(screen.getByText("99+")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,63 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/rutorrent/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "rutorrent" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("rutorrent.active")).toBeInTheDocument();
expect(screen.getByText("rutorrent.upload")).toBeInTheDocument();
expect(screen.getByText("rutorrent.download")).toBeInTheDocument();
});
it("renders error UI when widget API errors", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
renderWithProviders(<Component service={{ widget: { type: "rutorrent" } }} />, { settings: { hideErrors: false } });
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
});
it("renders computed active/upload/download values", () => {
useWidgetAPI.mockReturnValue({
data: [
{ "d.get_state": "1", "d.get_up_rate": "10", "d.get_down_rate": "5" },
{ "d.get_state": "0", "d.get_up_rate": "20", "d.get_down_rate": "15" },
{ "d.get_state": "1", "d.get_up_rate": "0", "d.get_down_rate": "0" },
],
error: undefined,
});
const { container } = renderWithProviders(<Component service={{ widget: { type: "rutorrent" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("2")).toBeInTheDocument(); // active torrents
expect(screen.getByText("30")).toBeInTheDocument(); // upload sum (common.byterate mocked)
expect(screen.getByText("20")).toBeInTheDocument(); // download sum (common.byterate mocked)
});
});

View File

@@ -0,0 +1,53 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/swagdashboard/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "swagdashboard" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("swagdashboard.proxied")).toBeInTheDocument();
expect(screen.getByText("swagdashboard.auth")).toBeInTheDocument();
expect(screen.getByText("swagdashboard.outdated")).toBeInTheDocument();
expect(screen.getByText("swagdashboard.banned")).toBeInTheDocument();
});
it("renders values when loaded", () => {
useWidgetAPI.mockReturnValue({
data: { proxied: 1, auth: 2, outdated: 3, banned: 4 },
error: undefined,
});
const { container } = renderWithProviders(<Component service={{ widget: { type: "swagdashboard" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("4")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,81 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
// Pool is rendered outside of the main Container; stub it to a simple marker.
vi.mock("widgets/truenas/pool", () => ({
default: ({ name, healthy, allocated, free }) => (
<div
data-testid="truenas-pool"
data-name={name}
data-healthy={String(healthy)}
data-allocated={allocated}
data-free={free}
/>
),
}));
import Component from "./component";
describe("widgets/truenas/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading (no pools)", () => {
useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
const { container } = renderWithProviders(<Component service={{ widget: { type: "truenas" } }} />, {
settings: { hideErrors: false },
});
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("truenas.load")).toBeInTheDocument();
expect(screen.getByText("truenas.uptime")).toBeInTheDocument();
expect(screen.getByText("truenas.alerts")).toBeInTheDocument();
expect(screen.queryByTestId("truenas-pool")).toBeNull();
});
it("renders values and pool list when enablePools is on and data is present", () => {
useWidgetAPI.mockImplementation((widget, endpoint) => {
if (endpoint === "alerts") return { data: { pending: 7 }, error: undefined };
if (endpoint === "status") return { data: { loadavg: [1.23], uptime_seconds: 3600 }, error: undefined };
if (endpoint === "pools") return { data: [{ id: "1", name: "tank", healthy: true }], error: undefined };
if (endpoint === "dataset")
return {
data: [{ pool: "tank", name: "tank", used: { parsed: 10 }, available: { parsed: 20 } }],
error: undefined,
};
return { data: undefined, error: undefined };
});
const { container } = renderWithProviders(
<Component service={{ widget: { type: "truenas", enablePools: true } }} />,
{
settings: { hideErrors: false },
},
);
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("1.23")).toBeInTheDocument();
expect(screen.getByText("3600")).toBeInTheDocument(); // common.duration mocked
expect(screen.getByText("7")).toBeInTheDocument();
const pool = screen.getByTestId("truenas-pool");
expect(pool.getAttribute("data-name")).toBe("tank");
expect(pool.getAttribute("data-healthy")).toBe("true");
expect(pool.getAttribute("data-allocated")).toBe("10");
expect(pool.getAttribute("data-free")).toBe("20");
});
});

View File

@@ -0,0 +1,67 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import Component from "./component";
describe("widgets/unraid/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("defaults widget.fields and filters down to 4 visible blocks while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "unraid" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
// Component sets default fields
expect(service.widget.fields).toEqual(["status", "cpu", "memoryPercent", "notifications"]);
// Container filters the many placeholder Blocks down to the selected fields.
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("unraid.status")).toBeInTheDocument();
expect(screen.getByText("unraid.cpu")).toBeInTheDocument();
expect(screen.getByText("unraid.notifications")).toBeInTheDocument();
expect(screen.getByText("unraid.memoryUsed")).toBeInTheDocument();
expect(screen.queryByText("unraid.memoryAvailable")).toBeNull();
});
it("renders values for the default fields", () => {
useWidgetAPI.mockReturnValue({
data: {
arrayState: "started",
cpuPercent: 12,
memoryAvailable: 100,
memoryUsed: 50,
memoryUsedPercent: 33,
unreadNotifications: 7,
arrayUsed: 1,
arrayFree: 2,
arrayUsedPercent: 3,
caches: {},
},
error: undefined,
});
const service = { widget: { type: "unraid" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("unraid.started")).toBeInTheDocument();
expect(screen.getByText("12")).toBeInTheDocument();
expect(screen.getByText("33")).toBeInTheDocument();
expect(screen.getByText("7")).toBeInTheDocument();
});
});

View File

@@ -4,7 +4,8 @@ import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
afterEach(() => {
cleanup();
// Node-environment tests shouldn't require jsdom; guard cleanup accordingly.
if (typeof document !== "undefined") cleanup();
});
// implement a couple of common formatters mocked in next-i18next
@@ -13,6 +14,10 @@ vi.mock("next-i18next", () => ({
t: (key, opts) => {
if (key === "common.number") return String(opts?.value ?? "");
if (key === "common.percent") return String(opts?.value ?? "");
if (key === "common.bytes") return String(opts?.value ?? "");
if (key === "common.bbytes") return String(opts?.value ?? "");
if (key === "common.byterate") return String(opts?.value ?? "");
if (key === "common.duration") return String(opts?.value ?? "");
return key;
},
}),