mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-29 23:41:16 -07:00
Feature: add Tracearr widget for displaying active Plex streams (#6306)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
// @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";
|
||||
|
||||
vi.mock("react-icons/bs", () => ({
|
||||
BsCpu: (props) => <svg data-testid="BsCpu" {...props} />,
|
||||
BsFillCpuFill: (props) => <svg data-testid="BsFillCpuFill" {...props} />,
|
||||
BsFillPlayFill: (props) => <svg data-testid="BsFillPlayFill" {...props} />,
|
||||
BsPauseFill: (props) => <svg data-testid="BsPauseFill" {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock("react-icons/md", () => ({
|
||||
MdOutlineSmartDisplay: (props) => <svg data-testid="MdOutlineSmartDisplay" {...props} />,
|
||||
MdSmartDisplay: (props) => <svg data-testid="MdSmartDisplay" {...props} />,
|
||||
}));
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
describe("widgets/tracearr/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders placeholder rows while loading", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getAllByText("-").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders placeholder blocks while loading in summary view", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: { data: null }, error: undefined });
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr", view: "summary" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByText("tracearr.streams")).toBeInTheDocument();
|
||||
expect(screen.getByText("tracearr.transcodes")).toBeInTheDocument();
|
||||
expect(screen.getByText("tracearr.directplay")).toBeInTheDocument();
|
||||
expect(screen.getByText("tracearr.bitrate")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders errors from the widget API", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "boom" } });
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByText(/widget\.api_error\s+widget\.information/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/boom/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders no-active message when there are no streams", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: { data: [], summary: { total: 0, transcodes: 0, directPlays: 0, totalBitrate: "0 Mbps" } },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByText("tracearr.no_active")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an expanded two-row entry when a single stream is playing", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000, // 2 hours
|
||||
progressMs: 2700000, // 45 minutes in
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByText("Inception")).toBeInTheDocument();
|
||||
expect(screen.getByText(/45:00/)).toBeInTheDocument(); // 45 minutes in
|
||||
expect(screen.getByText(/02:00:00/)).toBeInTheDocument(); // 2 hour duration
|
||||
});
|
||||
|
||||
it("uses 0% progress when duration is 0 in expanded view", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Short Clip",
|
||||
mediaType: "movie",
|
||||
durationMs: 0,
|
||||
progressMs: 5000,
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "1 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
const bars = container.querySelectorAll('div[style*="width"]');
|
||||
expect(bars.length).toBeGreaterThan(0);
|
||||
expect(bars[0]).toHaveStyle({ width: "0%" });
|
||||
});
|
||||
|
||||
it("renders episode title with season/episode and username when configured", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "2",
|
||||
mediaTitle: "Ozymandias",
|
||||
showTitle: "Breaking Bad",
|
||||
mediaType: "episode",
|
||||
seasonNumber: 5,
|
||||
episodeNumber: 14,
|
||||
durationMs: 2700000,
|
||||
progressMs: 1200000,
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
username: "Walter",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "10 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<Component
|
||||
service={{
|
||||
widget: { type: "tracearr", enableUser: true, showEpisodeNumber: true, expandOneStreamToTwoRows: false },
|
||||
}}
|
||||
/>,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(screen.getByText("Breaking Bad: S05 · E14 - Ozymandias (Walter)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders multiple streams including movie and tv episode", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000, // 2 hours
|
||||
progressMs: 2700000, // 45 minutes in
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
mediaTitle: "Ozymandias",
|
||||
showTitle: "Breaking Bad",
|
||||
mediaType: "episode",
|
||||
seasonNumber: 5,
|
||||
episodeNumber: 14,
|
||||
durationMs: 2700000, // 45 minutes
|
||||
progressMs: 1200000, // 20 minutes in
|
||||
state: "playing",
|
||||
videoDecision: "transcode",
|
||||
audioDecision: "directplay",
|
||||
username: "Walter",
|
||||
},
|
||||
],
|
||||
summary: { total: 2, transcodes: 1, directPlays: 1, totalBitrate: "35 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByText("Inception")).toBeInTheDocument();
|
||||
expect(screen.getByText("Breaking Bad - Ozymandias")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["copy/copy shows copy icon", { videoDecision: "copy", audioDecision: "copy" }, "MdOutlineSmartDisplay"],
|
||||
["transcode shows cpu fill icon", { videoDecision: "transcode", audioDecision: "directplay" }, "BsFillCpuFill"],
|
||||
["transcode+copy shows cpu fill icon", { videoDecision: "transcode", audioDecision: "copy" }, "BsFillCpuFill"],
|
||||
["mixed transcode shows cpu icon", { videoDecision: "directplay", audioDecision: "transcode" }, "BsCpu"],
|
||||
])("renders transcoding indicators in expanded view: %s", (_label, decisions, expectedIcon) => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000,
|
||||
progressMs: 2700000,
|
||||
state: "playing",
|
||||
...decisions,
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByTestId(expectedIcon)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a pause icon when a stream is paused in expanded view", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000,
|
||||
progressMs: 2700000,
|
||||
state: "paused",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr" } }} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(screen.getByTestId("BsPauseFill")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["copy/copy shows copy icon", { videoDecision: "copy", audioDecision: "copy" }, "MdOutlineSmartDisplay"],
|
||||
["transcode shows cpu fill icon", { videoDecision: "transcode", audioDecision: "directplay" }, "BsFillCpuFill"],
|
||||
["transcode+copy shows cpu fill icon", { videoDecision: "transcode", audioDecision: "copy" }, "BsFillCpuFill"],
|
||||
["mixed transcode shows cpu icon", { videoDecision: "directplay", audioDecision: "transcode" }, "BsCpu"],
|
||||
])("renders transcoding indicators in single-row view: %s", (_label, decisions, expectedIcon) => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000,
|
||||
progressMs: 2700000,
|
||||
state: "playing",
|
||||
...decisions,
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr", expandOneStreamToTwoRows: false } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId(expectedIcon)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a pause icon when a stream is paused in single-row view", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000,
|
||||
progressMs: 2700000,
|
||||
state: "paused",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr", expandOneStreamToTwoRows: false } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("BsPauseFill")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses 0% progress when duration is 0 in single-row view", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Short Clip",
|
||||
mediaType: "movie",
|
||||
durationMs: 0,
|
||||
progressMs: 5000,
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "1 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component service={{ widget: { type: "tracearr", expandOneStreamToTwoRows: false } }} />,
|
||||
{
|
||||
settings: { hideErrors: false },
|
||||
},
|
||||
);
|
||||
|
||||
const bars = container.querySelectorAll('div[style*="width"]');
|
||||
expect(bars.length).toBeGreaterThan(0);
|
||||
expect(bars[0]).toHaveStyle({ width: "0%" });
|
||||
});
|
||||
|
||||
it("renders summary view when view option is set to summary", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [],
|
||||
summary: { total: 5, transcodes: 2, directPlays: 3, totalBitrate: "45 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr", view: "summary" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByText("tracearr.streams")).toBeInTheDocument();
|
||||
expect(screen.getByText("tracearr.bitrate")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders both summary and details when view option is set to both", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "1",
|
||||
mediaTitle: "Inception",
|
||||
mediaType: "movie",
|
||||
durationMs: 7200000,
|
||||
progressMs: 2700000,
|
||||
state: "playing",
|
||||
videoDecision: "directplay",
|
||||
audioDecision: "directplay",
|
||||
},
|
||||
],
|
||||
summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "tracearr", view: "both" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByText("tracearr.streams")).toBeInTheDocument();
|
||||
expect(screen.getByText("Inception")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user