Files
manage/frontend/src/widgets/__tests__/JellyfinNowPlayingWidget.test.tsx
T
Developer b877a32ad8 Add Jellyfin Now Playing + Grafana Panel embed widgets
Two new additive widget kinds:

Jellyfin 'now_playing': like the existing 'activity' widget but filters
to only sessions with active playback (NowPlayingItem present + not
paused). Shows who's actually watching right now. The 'activity' kind
is unchanged (shows all sessions including idle).

Grafana 'panel': embeds a single Grafana panel directly in the app via
an iframe, using Grafana's /d-solo/ endpoint (renders one panel without
dashboard chrome, kiosk=tv). Configurable dashboard_uid, panel_id, and
time range (from/to, defaults now-1h/now). Includes a fallback 'Open in
Grafana' link for when embedding is blocked by X-Frame-Options/CSP. The
'link' kind is unchanged (still builds a deep-link URL).

Backend: new widget configs + definitions on jellyfin/grafana; source
adapter logic (session filter for now_playing; d-solo embed URL for
panel); 6 new tests.

Frontend: JellyfinNowPlayingWidget + GrafanaPanelWidget components;
registry bindings; 6 new tests.

278 backend tests pass (+6); 127 frontend tests pass (+6); lint/build
green both sides.
2026-06-28 15:26:43 +00:00

90 lines
2.2 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { JellyfinNowPlayingWidget } from "../JellyfinNowPlayingWidget";
import type { WidgetInstance } from "../../types";
vi.mock("../../hooks/useWidgets", () => ({
useWidgetData: vi.fn(),
}));
const widget: WidgetInstance = {
id: "w1",
service_id: "s1",
widget_kind: "now_playing",
title: "Now Playing",
config: {},
enabled: true,
sort_order: 0,
created_at: 0,
updated_at: 0,
};
describe("JellyfinNowPlayingWidget", () => {
it("renders sessions when data is present", async () => {
const { useWidgetData } = await import("../../hooks/useWidgets");
vi.mocked(useWidgetData).mockReturnValue({
data: {
widget_id: "w1",
data: {
sessions: [
{
user: "alice",
title: "Movie",
state: "playing",
type: "Movie",
device: "Web",
session_id: "s1",
transcoding: "no",
transcoding_type: "",
},
],
},
fetched_at: 0,
},
isLoading: false,
} as never);
render(
<JellyfinNowPlayingWidget widget={widget} refreshIntervalMs={30000} />,
);
expect(screen.getByText("alice")).toBeInTheDocument();
expect(screen.getAllByText("Movie").length).toBeGreaterThan(0);
});
it("renders the now-playing empty message when no sessions", async () => {
const { useWidgetData } = await import("../../hooks/useWidgets");
vi.mocked(useWidgetData).mockReturnValue({
data: {
widget_id: "w1",
data: { sessions: [] },
fetched_at: 0,
},
isLoading: false,
} as never);
render(
<JellyfinNowPlayingWidget widget={widget} refreshIntervalMs={30000} />,
);
expect(
screen.getByText("No one is playing right now."),
).toBeInTheDocument();
});
it("renders error state", async () => {
const { useWidgetData } = await import("../../hooks/useWidgets");
vi.mocked(useWidgetData).mockReturnValue({
data: {
widget_id: "w1",
error: "Connection failed",
fetched_at: 0,
},
isLoading: false,
} as never);
render(
<JellyfinNowPlayingWidget widget={widget} refreshIntervalMs={30000} />,
);
expect(screen.getByText("Connection failed")).toBeInTheDocument();
});
});