import type { Job } from "@shared/types.js"; 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 * as api from "../api"; import { OrchestratorPage } from "./OrchestratorPage"; import type { FilterTab } from "./orchestrator/constants"; vi.mock("../api", () => ({ updateSettings: vi.fn().mockResolvedValue({}), runPipeline: vi.fn().mockResolvedValue({ message: "ok" }), cancelPipeline: vi.fn().mockResolvedValue({ message: "Pipeline cancellation requested", pipelineRunId: "run-1", alreadyRequested: false, }), getPipelineStatus: vi.fn().mockResolvedValue({ isRunning: false, lastRun: null, nextScheduledRun: null, }), })); let mockIsPipelineRunning = false; 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 processingJob: Job = { ...jobFixture, id: "job-3", status: "processing" }; 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, processingJob], stats: { discovered: 1, processing: 1, ready: 1, applied: 0, skipped: 0, expired: 0, }, isLoading: false, isPipelineRunning: mockIsPipelineRunning, 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, }, refreshSettings: vi.fn(), }), })); vi.mock("./orchestrator/OrchestratorHeader", () => ({ OrchestratorHeader: ({ onCancelPipeline, }: { onCancelPipeline: () => void; }) => (
), })); 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, onToggleSelectJob, onToggleSelectAll, selectedJobId, }: { onSelectJob: (id: string) => void; onToggleSelectJob: (id: string) => void; onToggleSelectAll: (checked: boolean) => void; selectedJobId: string | null; }) => (
{selectedJobId ?? "none"}
), })); vi.mock("./orchestrator/RunModeModal", () => ({ RunModeModal: ({ onSaveAndRunAutomatic, }: { onSaveAndRunAutomatic: (values: { topN: number; minSuitabilityScore: number; searchTerms: string[]; runBudget: number; }) => Promise; }) => ( ), })); vi.mock("../components", () => ({ ManualImportSheet: () =>
, })); const LocationWatcher = () => { const location = useLocation(); return (
{location.pathname + location.search}
); }; describe("OrchestratorPage", () => { beforeEach(() => { vi.clearAllMocks(); mockIsPipelineRunning = false; }); 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("requests pipeline cancellation when running", async () => { mockIsPipelineRunning = true; window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); fireEvent.click(screen.getByText("Cancel Pipeline")); await waitFor(() => { expect(api.cancelPipeline).toHaveBeenCalledTimes(1); }); }); 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", ); }); }); it("saves automatic settings from modal", async () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; const setIntervalSpy = vi .spyOn(globalThis, "setInterval") .mockReturnValue(0 as unknown as NodeJS.Timeout); render( } /> } /> , ); fireEvent.click(screen.getByTestId("run-automatic")); await waitFor(() => { expect(api.updateSettings).toHaveBeenCalledWith({ searchTerms: ["backend"], jobspyResultsWanted: 150, gradcrackerMaxJobsPerTerm: 150, ukvisajobsMaxJobs: 150, }); }); setIntervalSpy.mockRestore(); }); it("shows and hides bulk Recalculate match based on selected statuses", async () => { window.matchMedia = createMatchMedia( true, ) as unknown as typeof window.matchMedia; render( } /> } /> , ); fireEvent.click(screen.getByTestId("toggle-select-all-on")); await waitFor(() => { expect( screen.queryByRole("button", { name: "Recalculate match" }), ).not.toBeInTheDocument(); }); fireEvent.click(screen.getByTestId("toggle-select-all-off")); fireEvent.click(screen.getByTestId("toggle-select-job-1")); await waitFor(() => { expect( screen.getByRole("button", { name: "Recalculate match" }), ).toBeInTheDocument(); }); }); });