197 lines
6.9 KiB
TypeScript
197 lines
6.9 KiB
TypeScript
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="[^"]+"/);
|
||
|
|
});
|
||
|
|
});
|