orchestrator tests
This commit is contained in:
parent
a4f52b923a
commit
84987f5921
164
orchestrator/src/client/pages/OrchestratorPage.test.tsx
Normal file
164
orchestrator/src/client/pages/OrchestratorPage.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
140
orchestrator/src/client/pages/orchestrator/JobListPanel.test.tsx
Normal file
140
orchestrator/src/client/pages/orchestrator/JobListPanel.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -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" });
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user