Files
silo-server/web/src/components/RequestToAddSection.test.tsx
T

197 lines
6.9 KiB
TypeScript
Raw Normal View History

import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const mocks = vi.hoisted(() => ({
useCanRequest: vi.fn(),
useRequestSearch: vi.fn(),
useDebounce: vi.fn(),
}));
vi.mock("@/hooks/useCanRequest", () => ({
useCanRequest: () => mocks.useCanRequest(),
}));
vi.mock("@/hooks/queries/useRequests", () => ({
useRequestSearch: (...args: unknown[]) => mocks.useRequestSearch(...args),
}));
vi.mock("@/hooks/useDebounce", () => ({
useDebounce: <T,>(v: T) => mocks.useDebounce(v) ?? v,
}));
import { RequestToAddSection } from "./RequestToAddSection";
import type { RequestMediaResult } from "@/api/types";
function render(child: ReactNode) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return renderToStaticMarkup(
<QueryClientProvider client={client}>
<MemoryRouter>{child}</MemoryRouter>
</QueryClientProvider>,
);
}
const missingResult = (overrides: Partial<RequestMediaResult> = {}): RequestMediaResult => ({
media_type: "movie",
tmdb_id: 1,
title: "Dune: Prophecy",
year: 2024,
availability: "missing",
request: { requestable: true },
...overrides,
});
const availableResult = (overrides: Partial<RequestMediaResult> = {}): RequestMediaResult => ({
media_type: "movie",
tmdb_id: 2,
title: "Dune",
year: 2021,
availability: "available",
request: { requestable: false },
...overrides,
});
describe("RequestToAddSection (dialog variant)", () => {
beforeEach(() => {
mocks.useCanRequest.mockReset();
mocks.useRequestSearch.mockReset();
mocks.useDebounce.mockReset();
mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
mocks.useDebounce.mockImplementation((v: unknown) => v);
});
it("renders nothing when discovery is disabled", () => {
mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
mocks.useRequestSearch.mockReturnValue({ data: undefined, isLoading: false, isError: false });
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toBe("");
});
it("passes enabled=false to useRequestSearch when discovery is disabled so no network call fires", () => {
mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
mocks.useRequestSearch.mockReturnValue({ data: undefined, isLoading: false, isError: false });
render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
const call = mocks.useRequestSearch.mock.calls.at(-1);
expect(call?.[0]).toBe("all");
expect(call?.[1]).toBe("dune");
expect(call?.[2]).toBe(1);
expect(call?.[3]).toEqual({ enabled: false });
});
it("passes enabled=true to useRequestSearch when discovery is enabled", () => {
mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
mocks.useRequestSearch.mockReturnValue({
data: { page: 1, total_pages: 1, total_results: 0, results: [] },
isLoading: false,
isError: false,
});
render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
const call = mocks.useRequestSearch.mock.calls.at(-1);
expect(call?.[3]).toEqual({ enabled: true });
});
it("renders 'Request to Add' header when library had hits", () => {
mocks.useRequestSearch.mockReturnValue({
data: { page: 1, total_pages: 1, total_results: 1, results: [missingResult()] },
isLoading: false,
isError: false,
});
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toContain("Request to Add");
expect(markup).toContain("Dune: Prophecy");
});
it("renders soft framing when library had 0 hits", () => {
mocks.useRequestSearch.mockReturnValue({
data: { page: 1, total_pages: 1, total_results: 1, results: [missingResult()] },
isLoading: false,
isError: false,
});
const markup = render(
<RequestToAddSection variant="dialog" query="dune" libraryHadHits={false} />,
);
expect(markup).toContain("Not in your library, but you can request");
expect(markup).not.toContain("Request to Add");
});
it("filters out results already available in the library", () => {
mocks.useRequestSearch.mockReturnValue({
data: {
page: 1,
total_pages: 1,
total_results: 2,
results: [availableResult(), missingResult()],
},
isLoading: false,
isError: false,
});
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toContain("Dune: Prophecy");
expect(markup).not.toContain('title="Dune"');
});
it("renders nothing when TMDB returned an error", () => {
mocks.useRequestSearch.mockReturnValue({ data: undefined, isLoading: false, isError: true });
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toBe("");
});
it("renders nothing when all TMDB results are already in the library", () => {
mocks.useRequestSearch.mockReturnValue({
data: { page: 1, total_pages: 1, total_results: 1, results: [availableResult()] },
isLoading: false,
isError: false,
});
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toBe("");
});
it("limits the dialog variant to at most 4 rows", () => {
const many = Array.from({ length: 10 }, (_, i) =>
missingResult({ tmdb_id: i + 100, title: `Result ${i}` }),
);
mocks.useRequestSearch.mockReturnValue({
data: { page: 1, total_pages: 1, total_results: many.length, results: many },
isLoading: false,
isError: false,
});
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toContain("Result 0");
expect(markup).toContain("Result 3");
expect(markup).not.toContain("Result 4");
});
it("renders the disabled affordance and reason when a row is not requestable", () => {
mocks.useRequestSearch.mockReturnValue({
data: {
page: 1,
total_pages: 1,
total_results: 1,
results: [
missingResult({
tmdb_id: 7,
title: "Quota Capped Movie",
request: { requestable: false, reason: "quota_exhausted" },
}),
],
},
isLoading: false,
isError: false,
});
const markup = render(<RequestToAddSection variant="dialog" query="dune" libraryHadHits />);
expect(markup).toContain("Quota Capped Movie");
expect(markup).not.toContain("bg-amber-400/15");
expect(markup).toMatch(/title="[^"]+"/);
});
});