test: cover widget internal modules (calendar, openwrt, omv, truenas, jdownloader)

This commit is contained in:
shamoon
2026-02-04 10:32:29 -08:00
parent e93a1c04e2
commit 0b332f95f4
15 changed files with 685 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { DateTime } from "luxon";
import { describe, expect, it, vi } from "vitest";
const { EventStub, compareDateTimezoneStub } = vi.hoisted(() => ({
EventStub: vi.fn(({ event, showDate, showTime }) => (
<div data-testid="event" data-showdate={showDate ? "1" : "0"} data-showtime={showTime ? "1" : "0"}>
{event.title}
</div>
)),
compareDateTimezoneStub: vi.fn(
(date, event) => date.startOf("day").toISODate() === event.date.startOf("day").toISODate(),
),
}));
vi.mock("./event", () => ({
default: EventStub,
compareDateTimezone: compareDateTimezoneStub,
}));
import Agenda from "./agenda";
describe("widgets/calendar/agenda", () => {
it("renders an empty placeholder when showDate is not set", () => {
const { container } = render(<Agenda service={{ widget: {} }} colorVariants={{}} events={{}} showDate={null} />);
expect(container.textContent).toBe("");
});
it("renders a no-events placeholder when there are no events in range", () => {
render(<Agenda service={{ widget: {} }} colorVariants={{}} events={{}} showDate={DateTime.now()} />);
expect(screen.getByText("calendar.noEventsToday")).toBeInTheDocument();
expect(EventStub).toHaveBeenCalled();
});
it("filters by previousDays, sorts, and enforces maxEvents", () => {
const showDate = DateTime.local(2099, 1, 2).startOf("day");
const service = { widget: { previousDays: 0, maxEvents: 2, showTime: true } };
const events = {
old: { title: "Old", date: DateTime.local(2099, 1, 1, 0, 0), color: "gray" },
a: { title: "A", date: DateTime.local(2099, 1, 2, 10, 0), color: "gray" },
b: { title: "B", date: DateTime.local(2099, 1, 3, 10, 0), color: "gray" },
c: { title: "C", date: DateTime.local(2099, 1, 4, 10, 0), color: "gray" },
};
render(<Agenda service={service} colorVariants={{}} events={events} showDate={showDate} />);
// Old is filtered out, C is sliced out by maxEvents.
expect(screen.queryByText("Old")).toBeNull();
expect(screen.getByText("A")).toBeInTheDocument();
expect(screen.getByText("B")).toBeInTheDocument();
expect(screen.queryByText("C")).toBeNull();
const renderedEvents = screen.getAllByTestId("event");
expect(renderedEvents).toHaveLength(2);
// showTime is only true for the selected day.
const [first, second] = renderedEvents;
expect(first).toHaveAttribute("data-showtime", "1");
expect(second).toHaveAttribute("data-showtime", "0");
});
});

View File

@@ -0,0 +1,56 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from "@testing-library/react";
import { DateTime } from "luxon";
import { describe, expect, it } from "vitest";
import Event, { compareDateTimezone } from "./event";
describe("widgets/calendar/event", () => {
it("renders an anchor when a url is provided and toggles additional text on hover", () => {
const date = DateTime.fromISO("2099-01-01T13:00:00.000Z").setZone("utc");
render(
<Event
event={{
title: "Primary",
additional: "More info",
date,
color: "gray",
url: "https://example.com",
isCompleted: true,
}}
colorVariants={{ gray: "bg-gray-500" }}
showDate
showTime
/>,
);
const link = screen.getByRole("link", { name: /primary/i });
expect(link).toHaveAttribute("href", "https://example.com");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
// time is rendered when showTime=true
expect(link.textContent).toContain("13:00");
// default shows title, hover shows `additional`
expect(screen.getByText("Primary")).toBeInTheDocument();
expect(screen.queryByText("More info")).toBeNull();
fireEvent.mouseEnter(link);
expect(screen.getByText("More info")).toBeInTheDocument();
fireEvent.mouseLeave(link);
expect(screen.getByText("Primary")).toBeInTheDocument();
// completed icon from react-icons renders an SVG
expect(link.querySelector("svg")).toBeTruthy();
});
it("compareDateTimezone matches dates by day", () => {
const day = DateTime.fromISO("2099-01-01T00:00:00.000Z").setZone("utc");
expect(compareDateTimezone(day, { date: DateTime.fromISO("2099-01-01T23:59:00.000Z").setZone("utc") })).toBe(true);
expect(compareDateTimezone(day, { date: DateTime.fromISO("2099-01-02T00:00:00.000Z").setZone("utc") })).toBe(false);
});
});

View File

@@ -0,0 +1,64 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./ical";
describe("widgets/calendar/integrations/ical", () => {
it("adds parsed events within the date range", async () => {
useWidgetAPI.mockReturnValue({
data: {
data: [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Test//EN",
"BEGIN:VEVENT",
"UID:uid1",
"DTSTAMP:20990101T000000Z",
"DTSTART:20990101T130000Z",
"DTEND:20990101T140000Z",
"SUMMARY:Test Event",
"LOCATION:Office",
"URL:https://example.com",
"END:VEVENT",
"END:VCALENDAR",
"",
].join("\n"),
},
error: undefined,
});
const setEvents = vi.fn();
render(
<Integration
config={{ name: "Work", type: "ical", color: "blue", params: { showName: true } }}
params={{ start: "2099-01-01T00:00:00.000Z", end: "2099-01-02T00:00:00.000Z" }}
setEvents={setEvents}
hideErrors
timezone="utc"
/>,
);
await waitFor(() => expect(setEvents).toHaveBeenCalled());
const updater = setEvents.mock.calls[0][0];
const next = updater({});
const entries = Object.values(next);
expect(entries).toHaveLength(1);
const [event] = entries;
expect(event.title).toBe("Work: Test Event");
expect(event.color).toBe("blue");
expect(event.type).toBe("ical");
expect(event.additional).toBe("Office");
expect(event.url).toBe("https://example.com");
expect(event.isCompleted).toBe(false);
});
});

View File

@@ -0,0 +1,39 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./lidarr";
describe("widgets/calendar/integrations/lidarr", () => {
it("adds release events", async () => {
useWidgetAPI.mockReturnValue({
data: [
{ artist: { artistName: "Artist" }, title: "Album", releaseDate: "2099-01-01T00:00:00.000Z", grabbed: true },
],
error: undefined,
});
const setEvents = vi.fn();
render(
<Integration
config={{ type: "lidarr", color: "green" }}
params={{ start: "2099-01-01T00:00:00.000Z", end: "2099-01-02T00:00:00.000Z" }}
setEvents={setEvents}
hideErrors
/>,
);
await waitFor(() => expect(setEvents).toHaveBeenCalled());
const next = setEvents.mock.calls[0][0]({});
expect(Object.keys(next)).toEqual(["Artist - Album"]);
expect(next["Artist - Album"].isCompleted).toBe(true);
});
});

View File

@@ -0,0 +1,49 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./radarr";
describe("widgets/calendar/integrations/radarr", () => {
it("adds cinema/physical/digital events", async () => {
useWidgetAPI.mockReturnValue({
data: [
{
title: "Movie",
titleSlug: "movie",
hasFile: false,
inCinemas: "2099-01-01T00:00:00.000Z",
physicalRelease: "2099-01-02T00:00:00.000Z",
digitalRelease: "2099-01-03T00:00:00.000Z",
},
],
error: undefined,
});
const setEvents = vi.fn();
render(
<Integration
config={{ type: "radarr", baseUrl: "https://radarr.example", color: "amber" }}
params={{ start: "2099-01-01T00:00:00.000Z", end: "2099-01-10T00:00:00.000Z" }}
setEvents={setEvents}
hideErrors
/>,
);
await waitFor(() => expect(setEvents).toHaveBeenCalled());
const next = setEvents.mock.calls[0][0]({});
const keys = Object.keys(next);
expect(keys.some((k) => k.includes("calendar.inCinemas"))).toBe(true);
expect(keys.some((k) => k.includes("calendar.physicalRelease"))).toBe(true);
expect(keys.some((k) => k.includes("calendar.digitalRelease"))).toBe(true);
expect(Object.values(next)[0].url).toBe("https://radarr.example/movie/movie");
});
});

View File

@@ -0,0 +1,48 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./readarr";
describe("widgets/calendar/integrations/readarr", () => {
it("adds release events with author name", async () => {
useWidgetAPI.mockReturnValue({
data: [
{
title: "Book",
seriesTitle: "Series",
releaseDate: "2099-01-01T00:00:00.000Z",
grabbed: false,
author: { authorName: "Author" },
authorTitle: "Author Book",
},
],
error: undefined,
});
const setEvents = vi.fn();
render(
<Integration
config={{ type: "readarr", color: "rose" }}
params={{ start: "2099-01-01T00:00:00.000Z", end: "2099-01-02T00:00:00.000Z" }}
setEvents={setEvents}
hideErrors
/>,
);
await waitFor(() => expect(setEvents).toHaveBeenCalled());
const next = setEvents.mock.calls[0][0]({});
const [key] = Object.keys(next);
expect(key).toContain("Author");
expect(key).toContain("Book");
expect(key).toContain("(Series)");
});
});

View File

@@ -0,0 +1,48 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
useWidgetAPI: vi.fn(),
}));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./sonarr";
describe("widgets/calendar/integrations/sonarr", () => {
it("adds episode events", async () => {
useWidgetAPI.mockReturnValue({
data: [
{
series: { title: "Show", titleSlug: "show" },
seasonNumber: 1,
episodeNumber: 2,
airDateUtc: "2099-01-01T00:00:00.000Z",
hasFile: true,
},
],
error: undefined,
});
const setEvents = vi.fn();
render(
<Integration
config={{ type: "sonarr", baseUrl: "https://sonarr.example", color: "teal" }}
params={{ start: "2099-01-01T00:00:00.000Z", end: "2099-01-02T00:00:00.000Z" }}
setEvents={setEvents}
hideErrors
/>,
);
await waitFor(() => expect(setEvents).toHaveBeenCalled());
const next = setEvents.mock.calls[0][0]({});
const [entry] = Object.values(next);
expect(entry.title).toBe("Show");
expect(entry.additional).toBe("S1 E2");
expect(entry.url).toBe("https://sonarr.example/series/show");
expect(entry.isCompleted).toBe(true);
});
});

View File

@@ -0,0 +1,71 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from "@testing-library/react";
import { DateTime } from "luxon";
import { describe, expect, it, vi } from "vitest";
const { EventStub, compareDateTimezoneStub } = vi.hoisted(() => ({
EventStub: vi.fn(({ event }) => <div data-testid="event">{event.title}</div>),
compareDateTimezoneStub: vi.fn(
(date, event) => date.startOf("day").toISODate() === event.date.startOf("day").toISODate(),
),
}));
vi.mock("./event", () => ({
default: EventStub,
compareDateTimezone: compareDateTimezoneStub,
}));
import Monthly from "./monthly";
describe("widgets/calendar/monthly", () => {
it("renders an empty placeholder when showDate is not set", () => {
const { container } = render(
<Monthly
service={{ widget: {} }}
colorVariants={{}}
events={{}}
showDate={null}
setShowDate={() => {}}
currentDate={DateTime.now()}
/>,
);
expect(container.textContent).toBe("");
});
it("navigates months and renders day events", () => {
const setShowDate = vi.fn();
const showDate = DateTime.local(2099, 2, 15).startOf("day");
const currentDate = DateTime.local(2099, 2, 4).startOf("day");
const service = { widget: { maxEvents: 10, showTime: false } };
const events = {
e1: { title: "Today Event", date: DateTime.local(2099, 2, 15, 10, 0), color: "zinc" },
e2: { title: "Other Event", date: DateTime.local(2099, 2, 16, 10, 0), color: "zinc" },
};
render(
<Monthly
service={service}
colorVariants={{}}
events={events}
showDate={showDate}
setShowDate={setShowDate}
currentDate={currentDate}
/>,
);
expect(screen.getByText("Today Event")).toBeInTheDocument();
expect(screen.queryByText("Other Event")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: ">" }));
expect(setShowDate).toHaveBeenCalled();
expect(setShowDate.mock.calls[0][0].toISODate()).toBe(showDate.plus({ months: 1 }).startOf("day").toISODate());
fireEvent.click(screen.getByRole("button", { name: "<" }));
expect(setShowDate.mock.calls[1][0].toISODate()).toBe(showDate.minus({ months: 1 }).startOf("day").toISODate());
fireEvent.click(screen.getByRole("button", { name: showDate.toFormat("MMMM y") }));
expect(setShowDate.mock.calls[2][0].toISODate()).toBe(currentDate.startOf("day").toISODate());
});
});

View File

@@ -0,0 +1,39 @@
import crypto from "crypto";
import { describe, expect, it, vi } from "vitest";
import { createEncryptionToken, decrypt, encrypt, sha256, uniqueRid, validateRid } from "./tools";
describe("widgets/jdownloader/tools", () => {
it("sha256 returns a 32-byte buffer", () => {
expect(sha256("hello")).toBeInstanceOf(Buffer);
expect(sha256("hello")).toHaveLength(32);
});
it("uniqueRid returns an integer", () => {
vi.spyOn(Math, "random").mockReturnValueOnce(0.123);
expect(uniqueRid()).toBeTypeOf("number");
expect(Number.isInteger(uniqueRid())).toBe(true);
Math.random.mockRestore();
});
it("validateRid throws when mismatched", () => {
expect(() => validateRid({ rid: 1 }, 2)).toThrow(/RequestID mismatch/i);
expect(validateRid({ rid: 5 }, 5)).toEqual({ rid: 5 });
});
it("encrypt/decrypt roundtrip with a 32-byte ivKey", () => {
const ivKey = crypto.randomBytes(32);
const plaintext = "secret";
const encrypted = encrypt(plaintext, ivKey);
const decrypted = decrypt(encrypted, ivKey);
expect(decrypted).toBe(plaintext);
});
it("createEncryptionToken merges buffers and hashes", () => {
const oldToken = Buffer.from("aa", "hex");
const updateToken = "bb";
const token = createEncryptionToken(oldToken, updateToken);
expect(token).toBeInstanceOf(Buffer);
expect(token).toHaveLength(32);
});
});

View File

@@ -0,0 +1,42 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { 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 "./downloader_get_downloadlist";
describe("widgets/openmediavault/methods/downloader_get_downloadlist", () => {
it("renders '-' values when data is missing", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "openmediavault" } }} />, {
settings: { hideErrors: false },
});
expect(screen.getByText("openmediavault.downloading")).toBeInTheDocument();
expect(screen.getByText("openmediavault.total")).toBeInTheDocument();
expect(screen.getAllByText("-").length).toBeGreaterThan(0);
});
it("counts downloading and total items", () => {
useWidgetAPI.mockReturnValue({
data: { response: { data: [{ downloading: true }, { downloading: false }, { downloading: true }] } },
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "openmediavault" } }} />, {
settings: { hideErrors: false },
});
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,35 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { 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 "./services_get_status";
describe("widgets/openmediavault/methods/services_get_status", () => {
it("counts running/stopped/total services", () => {
useWidgetAPI.mockReturnValue({
data: { response: { data: [{ running: true }, { running: false }, { running: true }] } },
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "openmediavault" } }} />, {
settings: { hideErrors: false },
});
expect(screen.getByText("openmediavault.running")).toBeInTheDocument();
expect(screen.getByText("openmediavault.stopped")).toBeInTheDocument();
expect(screen.getByText("openmediavault.total")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { 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 "./smart_get_list";
describe("widgets/openmediavault/methods/smart_get_list", () => {
it("counts passed/failed monitored disks", () => {
useWidgetAPI.mockReturnValue({
data: {
response: {
output: JSON.stringify({
data: [
{ monitor: true, overallstatus: "GOOD" },
{ monitor: true, overallstatus: "BAD" },
{ monitor: false, overallstatus: "BAD" },
],
}),
},
},
error: undefined,
});
renderWithProviders(<Component service={{ widget: { type: "openmediavault" } }} />, {
settings: { hideErrors: false },
});
expect(screen.getByText("openmediavault.passed")).toBeInTheDocument();
expect(screen.getByText("openmediavault.failed")).toBeInTheDocument();
expect(screen.getAllByText("1")).toHaveLength(2);
});
});

View File

@@ -0,0 +1,37 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { 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 "./interface";
describe("widgets/openwrt/methods/interface", () => {
it("returns null while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "openwrt" } }} />);
expect(container.firstChild).toBeNull();
});
it("renders status and byte counters when loaded", () => {
useWidgetAPI.mockReturnValue({ data: { up: true, bytesTx: 100, bytesRx: 200 }, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "openwrt" } }} />, { settings: { hideErrors: false } });
expect(screen.getByText("widget.status")).toBeInTheDocument();
expect(screen.getByText("openwrt.bytesTx")).toBeInTheDocument();
expect(screen.getByText("openwrt.bytesRx")).toBeInTheDocument();
// t("common.bytes") mock returns the numeric value as a string.
expect(screen.getByText("100")).toBeInTheDocument();
expect(screen.getByText("200")).toBeInTheDocument();
expect(screen.getByText("openwrt.up")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,33 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { 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 "./system";
describe("widgets/openwrt/methods/system", () => {
it("returns null while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "openwrt" } }} />);
expect(container.firstChild).toBeNull();
});
it("renders uptime and cpu load when loaded", () => {
useWidgetAPI.mockReturnValue({ data: { uptime: 123, cpuLoad: "0.5" }, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "openwrt" } }} />, { settings: { hideErrors: false } });
expect(screen.getByText("openwrt.uptime")).toBeInTheDocument();
expect(screen.getByText("openwrt.cpuLoad")).toBeInTheDocument();
expect(screen.getByText("123")).toBeInTheDocument();
expect(screen.getByText("0.5")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,19 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import Pool from "./pool";
describe("widgets/truenas/pool", () => {
it("renders pool name, usage percent, and status color", () => {
const { container } = render(<Pool name="tank" free={50} allocated={50} healthy={false} />);
expect(screen.getByText("tank")).toBeInTheDocument();
// 50 / 100 => 50%
expect(container.textContent).toContain("(50%)");
// status color reflects healthy=false
expect(container.querySelector(".bg-yellow-500")).toBeTruthy();
});
});