orchestrator tests

This commit is contained in:
DaKheera47 2026-01-20 06:42:18 +00:00
parent a4f52b923a
commit 84987f5921
5 changed files with 695 additions and 0 deletions

View File

@ -0,0 +1,164 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { OrchestratorPage } from "./OrchestratorPage";
import type { Job } from "../../shared/types";
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",
suitabilityScore: 90,
suitabilityReason: null,
tailoredSummary: null,
tailoredHeadline: null,
tailoredSkills: null,
selectedProjectIds: null,
pdfPath: null,
notionPageId: 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 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],
stats: {
discovered: 0,
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("./orchestrator/OrchestratorHeader", () => ({
OrchestratorHeader: () => <div data-testid="header" />,
}));
vi.mock("./orchestrator/OrchestratorSummary", () => ({
OrchestratorSummary: () => <div data-testid="summary" />,
}));
vi.mock("./orchestrator/OrchestratorFilters", () => ({
OrchestratorFilters: () => <div data-testid="filters" />,
}));
vi.mock("./orchestrator/JobDetailPanel", () => ({
JobDetailPanel: () => <div data-testid="detail-panel" />,
}));
vi.mock("./orchestrator/JobListPanel", () => ({
JobListPanel: ({ onSelectJob, selectedJobId }: { onSelectJob: (id: string) => void; selectedJobId: string | null }) => (
<div>
<div data-testid="selected-job">{selectedJobId ?? "none"}</div>
<button type="button" onClick={() => onSelectJob("job-1")}>Select job</button>
</div>
),
}));
vi.mock("../components", () => ({
ManualImportSheet: () => <div data-testid="manual-import" />,
}));
describe("OrchestratorPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("opens the detail drawer on mobile when a job is selected", () => {
window.matchMedia = createMatchMedia(false) as unknown as typeof window.matchMedia;
render(
<MemoryRouter>
<OrchestratorPage />
</MemoryRouter>
);
expect(screen.queryByTestId("detail-panel")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /select job/i }));
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(
<MemoryRouter>
<OrchestratorPage />
</MemoryRouter>
);
expect(screen.getByTestId("detail-panel")).toBeInTheDocument();
});
});

View File

@ -0,0 +1,274 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { JobDetailPanel } from "./JobDetailPanel";
import type { Job } from "../../../shared/types";
import * as api from "../../api";
vi.mock("@/components/ui/dropdown-menu", () => {
return {
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div role="menu">{children}</div>,
DropdownMenuItem: ({
children,
onSelect,
...props
}: {
children: React.ReactNode;
onSelect?: () => void;
}) => (
<button type="button" role="menuitem" onClick={() => onSelect?.()} {...props}>
{children}
</button>
),
DropdownMenuSeparator: () => <div role="separator" />,
};
});
vi.mock("../../components", () => ({
DiscoveredPanel: ({ job }: { job: Job | null }) => (
<div data-testid="discovered-panel">{job?.id ?? "no-job"}</div>
),
}));
vi.mock("../../components/ReadyPanel", () => ({
ReadyPanel: ({ onEditDescription }: { onEditDescription?: () => void }) => (
<div>
<div data-testid="ready-panel" />
<button type="button" onClick={() => onEditDescription?.()}>
Edit description
</button>
</div>
),
}));
vi.mock("../../components/TailoringEditor", () => ({
TailoringEditor: () => <div data-testid="tailoring-editor" />,
}));
vi.mock("@client/lib/jobCopy", () => ({
copyTextToClipboard: vi.fn().mockResolvedValue(undefined),
formatJobForWebhook: vi.fn(() => "payload"),
}));
vi.mock("../../api", () => ({
updateJob: vi.fn(),
processJob: vi.fn(),
generateJobPdf: vi.fn(),
markAsApplied: vi.fn(),
skipJob: vi.fn(),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
message: vi.fn(),
},
}));
const createJob = (overrides: Partial<Job> = {}): 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: "https://example.com/apply",
disciplines: null,
deadline: "2025-02-01",
salary: "GBP 50k",
location: "London",
degreeRequired: null,
starting: null,
jobDescription: "Build APIs",
status: "ready",
suitabilityScore: 82,
suitabilityReason: "Strong fit",
tailoredSummary: null,
tailoredHeadline: null,
tailoredSkills: null,
selectedProjectIds: null,
pdfPath: null,
notionPageId: 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",
...overrides,
});
describe("JobDetailPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the discovered panel when active tab is discovered", () => {
const job = createJob({ id: "job-99", status: "discovered" });
render(
<JobDetailPanel
activeTab="discovered"
activeJobs={[job]}
selectedJob={job}
onSelectJobId={vi.fn()}
onJobUpdated={vi.fn().mockResolvedValue(undefined)}
onSetActiveTab={vi.fn()}
/>
);
expect(screen.getByTestId("discovered-panel")).toHaveTextContent("job-99");
});
it("wires ready panel edit actions back to the page", () => {
const onSetActiveTab = vi.fn();
render(
<JobDetailPanel
activeTab="ready"
activeJobs={[]}
selectedJob={createJob({ status: "ready" })}
onSelectJobId={vi.fn()}
onJobUpdated={vi.fn().mockResolvedValue(undefined)}
onSetActiveTab={onSetActiveTab}
/>
);
fireEvent.click(screen.getByRole("button", { name: /edit description/i }));
expect(onSetActiveTab).toHaveBeenCalledWith("discovered");
});
it("shows an empty state when no job is selected", () => {
render(
<JobDetailPanel
activeTab="all"
activeJobs={[]}
selectedJob={null}
onSelectJobId={vi.fn()}
onJobUpdated={vi.fn().mockResolvedValue(undefined)}
onSetActiveTab={vi.fn()}
/>
);
expect(screen.getByText("No job selected")).toBeInTheDocument();
});
it("renders a stripped description preview for html content", () => {
render(
<JobDetailPanel
activeTab="all"
activeJobs={[]}
selectedJob={createJob({ jobDescription: "<p>Hello <strong>world</strong></p>" })}
onSelectJobId={vi.fn()}
onJobUpdated={vi.fn().mockResolvedValue(undefined)}
onSetActiveTab={vi.fn()}
/>
);
expect(screen.getByText("Hello world")).toBeInTheDocument();
});
it("saves an edited description", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
vi.mocked(api.updateJob).mockResolvedValue(undefined as any);
render(
<JobDetailPanel
activeTab="all"
activeJobs={[]}
selectedJob={createJob({ jobDescription: "Original" })}
onSelectJobId={vi.fn()}
onJobUpdated={onJobUpdated}
onSetActiveTab={vi.fn()}
/>
);
fireEvent.mouseDown(screen.getByRole("tab", { name: /description/i }));
fireEvent.click(await screen.findByRole("button", { name: /^edit$/i }));
fireEvent.change(screen.getByPlaceholderText("Enter job description..."), {
target: { value: "Updated description" },
});
fireEvent.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() =>
expect(api.updateJob).toHaveBeenCalledWith("job-1", { jobDescription: "Updated description" })
);
expect(onJobUpdated).toHaveBeenCalled();
});
it("marks a job as applied from the action button", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
vi.mocked(api.markAsApplied).mockResolvedValue(undefined as any);
render(
<JobDetailPanel
activeTab="all"
activeJobs={[]}
selectedJob={createJob({ status: "ready" })}
onSelectJobId={vi.fn()}
onJobUpdated={onJobUpdated}
onSetActiveTab={vi.fn()}
/>
);
fireEvent.click(screen.getByRole("button", { name: /applied/i }));
await waitFor(() => expect(api.markAsApplied).toHaveBeenCalledWith("job-1"));
expect(onJobUpdated).toHaveBeenCalled();
});
it("skips a job from the menu", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
vi.mocked(api.skipJob).mockResolvedValue(undefined as any);
render(
<JobDetailPanel
activeTab="all"
activeJobs={[]}
selectedJob={createJob({ status: "ready" })}
onSelectJobId={vi.fn()}
onJobUpdated={onJobUpdated}
onSetActiveTab={vi.fn()}
/>
);
fireEvent.pointerDown(screen.getByRole("button", { name: /more actions/i }));
const skipItem = await screen.findByRole("menuitem", { name: /skip job/i });
fireEvent.click(skipItem);
await waitFor(() => expect(api.skipJob).toHaveBeenCalledWith("job-1"));
expect(onJobUpdated).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,140 @@
import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { JobListPanel } from "./JobListPanel";
import type { Job } from "../../../shared/types";
const createJob = (overrides: Partial<Job> = {}): 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",
suitabilityScore: 72,
suitabilityReason: null,
tailoredSummary: null,
tailoredHeadline: null,
tailoredSkills: null,
selectedProjectIds: null,
pdfPath: null,
notionPageId: 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",
...overrides,
});
describe("JobListPanel", () => {
it("shows a loading state when fetching jobs", () => {
render(
<JobListPanel
isLoading
jobs={[]}
activeJobs={[]}
selectedJobId={null}
activeTab="ready"
searchQuery=""
onSelectJob={vi.fn()}
/>
);
expect(screen.getByText("Loading jobs...")).toBeInTheDocument();
});
it("shows the tab empty state copy when no jobs exist", () => {
render(
<JobListPanel
isLoading={false}
jobs={[]}
activeJobs={[]}
selectedJobId={null}
activeTab="ready"
searchQuery=""
onSelectJob={vi.fn()}
/>
);
expect(screen.getByText("No jobs found")).toBeInTheDocument();
expect(screen.getByText("Run the pipeline to discover and process new jobs.")).toBeInTheDocument();
});
it("shows the query-specific empty state when searching", () => {
render(
<JobListPanel
isLoading={false}
jobs={[]}
activeJobs={[]}
selectedJobId={null}
activeTab="ready"
searchQuery="iOS"
onSelectJob={vi.fn()}
/>
);
expect(screen.getByText('No jobs match "iOS".')).toBeInTheDocument();
});
it("renders jobs and notifies when a job is selected", () => {
const onSelectJob = vi.fn();
const jobs = [
createJob({ id: "job-1", title: "Backend Engineer" }),
createJob({ id: "job-2", title: "Frontend Engineer", employer: "Globex" }),
];
render(
<JobListPanel
isLoading={false}
jobs={jobs}
activeJobs={jobs}
selectedJobId="job-1"
activeTab="ready"
searchQuery=""
onSelectJob={onSelectJob}
/>
);
expect(screen.getByRole("button", { name: /Backend Engineer/i })).toHaveAttribute("aria-pressed", "true");
fireEvent.click(screen.getByRole("button", { name: /Frontend Engineer/i }));
expect(onSelectJob).toHaveBeenCalledWith("job-2");
});
});

View File

@ -0,0 +1,107 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import type { ComponentProps } from "react";
import { OrchestratorFilters } from "./OrchestratorFilters";
import type { FilterTab, JobSort } from "./constants";
vi.mock("@/components/ui/dropdown-menu", () => {
const React = require("react") as typeof import("react");
const RadioGroupContext = React.createContext<((value: string) => void) | null>(null);
return {
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div role="menu">{children}</div>,
DropdownMenuItem: ({
children,
onSelect,
...props
}: {
children: React.ReactNode;
onSelect?: () => void;
}) => (
<button type="button" role="menuitem" onClick={() => onSelect?.()} {...props}>
{children}
</button>
),
DropdownMenuLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuSeparator: () => <div role="separator" />,
DropdownMenuRadioGroup: ({
children,
onValueChange,
}: {
children: React.ReactNode;
onValueChange?: (value: string) => void;
}) => (
<RadioGroupContext.Provider value={onValueChange ?? null}>
<div role="radiogroup">{children}</div>
</RadioGroupContext.Provider>
),
DropdownMenuRadioItem: ({
children,
value,
}: {
children: React.ReactNode;
value: string;
}) => {
const onValueChange = React.useContext(RadioGroupContext);
return (
<button type="button" role="menuitemradio" onClick={() => onValueChange?.(value)}>
{children}
</button>
);
},
};
});
const renderFilters = (overrides?: Partial<ComponentProps<typeof OrchestratorFilters>>) => {
const props = {
activeTab: "ready" as FilterTab,
onTabChange: vi.fn(),
counts: {
ready: 2,
discovered: 1,
applied: 3,
all: 6,
},
searchQuery: "",
onSearchQueryChange: vi.fn(),
sourceFilter: "all" as const,
onSourceFilterChange: vi.fn(),
sort: { key: "score", direction: "desc" } as JobSort,
onSortChange: vi.fn(),
...overrides,
};
return {
props,
...render(<OrchestratorFilters {...props} />),
};
};
describe("OrchestratorFilters", () => {
it("notifies when tabs and search are updated", () => {
const { props } = renderFilters();
fireEvent.mouseDown(screen.getByRole("tab", { name: /applied/i }));
expect(props.onTabChange).toHaveBeenCalledWith("applied");
fireEvent.change(screen.getByPlaceholderText("Search..."), { target: { value: "Design" } });
expect(props.onSearchQueryChange).toHaveBeenCalledWith("Design");
});
it("updates source and sort selections", async () => {
const { props } = renderFilters();
fireEvent.pointerDown(screen.getByRole("button", { name: /all sources/i }));
fireEvent.click(await screen.findByRole("menuitemradio", { name: /LinkedIn/i }));
expect(props.onSourceFilterChange).toHaveBeenCalledWith("linkedin");
fireEvent.pointerDown(screen.getByRole("button", { name: /score/i }));
fireEvent.click(await screen.findByRole("menuitem", { name: /Direction:/i }));
expect(props.onSortChange).toHaveBeenCalledWith({ key: "score", direction: "asc" });
});
});

View File

@ -3,3 +3,13 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
if (typeof globalThis.ResizeObserver === "undefined") {
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
globalThis.ResizeObserver = ResizeObserver;
}