import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Job } from "../../shared/types"; import { OrchestratorPage } from "./OrchestratorPage"; import type { FilterTab } from "./orchestrator/constants"; const jobFixture: Job = { id: "job-1", source: "linkedin", sourceJobId: null, jobUrlDirect: null, datePosted: null, title: "Backend Engineer", employer: "Acme", employerUrl: null, jobUrl: "https://example.com/job", applicationLink: null, disciplines: null, deadline: null, salary: null, location: "London", degreeRequired: null, starting: null, jobDescription: "Build APIs", status: "ready", outcome: null, closedAt: null, suitabilityScore: 90, suitabilityReason: null, tailoredSummary: null, tailoredHeadline: null, tailoredSkills: null, selectedProjectIds: null, pdfPath: null, notionPageId: null, sponsorMatchScore: null, sponsorMatchNames: null, jobType: null, salarySource: null, salaryInterval: null, salaryMinAmount: null, salaryMaxAmount: null, salaryCurrency: null, isRemote: null, jobLevel: null, jobFunction: null, listingType: null, emails: null, companyIndustry: null, companyLogo: null, companyUrlDirect: null, companyAddresses: null, companyNumEmployees: null, companyRevenue: null, companyDescription: null, skills: null, experienceRange: null, companyRating: null, companyReviewsCount: null, vacancyCount: null, workFromHomeType: null, discoveredAt: "2025-01-01T00:00:00Z", processedAt: null, appliedAt: null, createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-01-02T00:00:00Z", }; const job2: Job = { ...jobFixture, id: "job-2", status: "discovered" }; const createMatchMedia = (matches: boolean) => vi.fn().mockImplementation((query: string) => ({ matches, media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), })); vi.mock("./orchestrator/useOrchestratorData", () => ({ useOrchestratorData: () => ({ jobs: [jobFixture, job2], stats: { discovered: 1, processing: 0, ready: 1, applied: 0, skipped: 0, expired: 0, }, isLoading: false, isPipelineRunning: false, setIsPipelineRunning: vi.fn(), loadJobs: vi.fn(), }), })); vi.mock("./orchestrator/usePipelineSources", () => ({ usePipelineSources: () => ({ pipelineSources: ["linkedin"], setPipelineSources: vi.fn(), toggleSource: vi.fn(), }), })); vi.mock("../hooks/useSettings", () => ({ useSettings: () => ({ settings: { jobspySites: ["indeed", "linkedin"], ukvisajobsEmail: null, ukvisajobsPasswordHint: null, }, }), })); vi.mock("./orchestrator/OrchestratorHeader", () => ({ OrchestratorHeader: () =>
, })); vi.mock("./orchestrator/OrchestratorSummary", () => ({ OrchestratorSummary: () =>
, })); vi.mock("./orchestrator/OrchestratorFilters", () => ({ OrchestratorFilters: ({ onTabChange, onSearchQueryChange, onSortChange, sourcesWithJobs, }: { onTabChange: (t: FilterTab) => void; onSearchQueryChange: (q: string) => void; onSortChange: (s: any) => void; sourcesWithJobs: string[]; }) => (
{sourcesWithJobs.join(",")}
), })); vi.mock("./orchestrator/JobDetailPanel", () => ({ JobDetailPanel: () =>
, })); vi.mock("./orchestrator/JobListPanel", () => ({ JobListPanel: ({ onSelectJob, selectedJobId, }: { onSelectJob: (id: string) => void; selectedJobId: string | null; }) => (
{selectedJobId ?? "none"}
), })); vi.mock("../components", () => ({ ManualImportSheet: () =>
, })); const LocationWatcher = () => { const location = useLocation(); return (
{location.pathname + location.search}
); }; describe("OrchestratorPage", () => { beforeEach(() => { vi.clearAllMocks(); }); it("syncs tab selection to the URL", () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); fireEvent.click(screen.getByText("To Discovered")); expect(screen.getByTestId("location").textContent).toContain("/discovered"); }); it("syncs job selection to the URL", async () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); // Initial load will auto-select the first matching job (job-1 for all tab) const locationText = () => screen.getByTestId("location").textContent; expect(locationText()).toContain("/all/job-1"); // Clicking job-2 should update URL const job2Button = screen.getByTestId("select-job-2"); fireEvent.click(job2Button); // Wait for URL to update await waitFor(() => { expect(locationText()).toContain("/all/job-2"); }); }); it("syncs search query to URL as a parameter", () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); fireEvent.click(screen.getByText("Set Search")); expect(screen.getByTestId("location").textContent).toContain( "q=test+search", ); }); it("syncs sorting to URL and removes it when default", () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); fireEvent.click(screen.getByText("Set Sort")); expect(screen.getByTestId("location").textContent).toContain( "sort=title-asc", ); }); it("opens the detail drawer on mobile when a job is selected", () => { window.matchMedia = createMatchMedia( false, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); expect(screen.queryByTestId("detail-panel")).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId("select-job-1")); expect(screen.getByTestId("detail-panel")).toBeInTheDocument(); }); it("renders the detail panel inline on desktop", () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); expect(screen.getByTestId("detail-panel")).toBeInTheDocument(); }); it("clears source filter when no jobs exist for it", async () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> , ); await waitFor(() => { expect(screen.getByTestId("location").textContent).not.toContain( "source=ukvisajobs", ); }); }); });