Jobber/orchestrator/src/client/components/ManualImportSheet.test.tsx
2026-01-25 12:41:44 +00:00

351 lines
11 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../api";
import { ManualImportSheet } from "./ManualImportSheet";
vi.mock("../api", () => ({
fetchJobFromUrl: vi.fn(),
inferManualJob: vi.fn(),
importManualJob: vi.fn(),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe("ManualImportSheet", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("runs analyze -> review -> import on the happy path", async () => {
const rawDescription = " Backend Engineer role in London. ";
const onOpenChange = vi.fn();
const onImported = vi.fn().mockResolvedValue(undefined);
vi.mocked(api.inferManualJob).mockResolvedValue({
job: {
title: "Backend Engineer",
employer: "Acme Labs",
location: "London, UK",
},
});
vi.mocked(api.importManualJob).mockResolvedValue({ id: "job-1" } as any);
render(
<ManualImportSheet
open
onOpenChange={onOpenChange}
onImported={onImported}
/>,
);
fireEvent.change(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
{ target: { value: rawDescription } },
);
fireEvent.click(screen.getByRole("button", { name: /analyze jd/i }));
const titleInput = await screen.findByPlaceholderText(
"e.g. Junior Backend Engineer",
);
expect(titleInput).toHaveValue("Backend Engineer");
const jdTextarea = screen.getByPlaceholderText(
"Paste the job description...",
) as HTMLTextAreaElement;
expect(jdTextarea.value).toBe(rawDescription.trim());
fireEvent.change(screen.getByPlaceholderText("e.g. GBP 45k-55k"), {
target: { value: " 120k " },
});
fireEvent.click(screen.getByRole("button", { name: /import job/i }));
await waitFor(() => expect(api.importManualJob).toHaveBeenCalled());
expect(api.importManualJob).toHaveBeenCalledWith({
job: expect.objectContaining({
title: "Backend Engineer",
employer: "Acme Labs",
location: "London, UK",
salary: "120k",
jobDescription: rawDescription.trim(),
}),
});
await waitFor(() => expect(onImported).toHaveBeenCalledWith("job-1"));
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(toast.success).toHaveBeenCalledWith(
"Job imported",
expect.objectContaining({
description: expect.any(String),
}),
);
});
it("shows warnings and requires required fields before import", async () => {
const rawDescription = "Manual QA Engineer role.";
vi.mocked(api.inferManualJob).mockResolvedValue({
job: {},
warning: "AI inference failed. Fill details manually.",
});
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
fireEvent.change(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
{ target: { value: rawDescription } },
);
fireEvent.click(screen.getByRole("button", { name: /analyze jd/i }));
await screen.findByText("AI inference failed. Fill details manually.");
const importButton = screen.getByRole("button", { name: /import job/i });
expect(importButton).toBeDisabled();
fireEvent.change(
screen.getByPlaceholderText("e.g. Junior Backend Engineer"),
{
target: { value: "QA Engineer" },
},
);
fireEvent.change(screen.getByPlaceholderText("e.g. Acme Labs"), {
target: { value: "Acme Labs" },
});
await waitFor(() => expect(importButton).toBeEnabled());
});
it("returns to the paste step when inference fails", async () => {
const rawDescription = "Backend role description.";
vi.mocked(api.inferManualJob).mockRejectedValue(
new Error("Inference failed"),
);
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
fireEvent.change(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
{ target: { value: rawDescription } },
);
fireEvent.click(screen.getByRole("button", { name: /analyze jd/i }));
await screen.findByText("Inference failed");
expect(
screen.getByRole("button", { name: /analyze jd/i }),
).toBeInTheDocument();
expect(
screen.queryByPlaceholderText("e.g. Junior Backend Engineer"),
).not.toBeInTheDocument();
});
it("shows a toast error and keeps the sheet open when import fails", async () => {
vi.mocked(api.inferManualJob).mockResolvedValue({
job: {
title: "Backend Engineer",
employer: "Acme Labs",
},
});
vi.mocked(api.importManualJob).mockRejectedValue(
new Error("Import failed"),
);
const onOpenChange = vi.fn();
render(
<ManualImportSheet
open
onOpenChange={onOpenChange}
onImported={vi.fn()}
/>,
);
fireEvent.change(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
{ target: { value: "Backend Engineer role." } },
);
fireEvent.click(screen.getByRole("button", { name: /analyze jd/i }));
await screen.findByPlaceholderText("e.g. Junior Backend Engineer");
fireEvent.click(screen.getByRole("button", { name: /import job/i }));
await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith("Import failed"),
);
expect(onOpenChange).not.toHaveBeenCalled();
expect(screen.getByRole("button", { name: /import job/i })).toBeEnabled();
});
describe("URL fetch functionality", () => {
it("shows Paste button when URL field is empty, Fetch when URL is entered", async () => {
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
// Initially should show Paste button
expect(
screen.getByRole("button", { name: /paste/i }),
).toBeInTheDocument();
// Enter a URL
fireEvent.change(
screen.getByPlaceholderText("https://example.com/job-posting"),
{ target: { value: "https://example.com/job" } },
);
// Should now show Fetch button
expect(
screen.getByRole("button", { name: /fetch/i }),
).toBeInTheDocument();
});
it("fetches URL and proceeds to review on successful fetch", async () => {
vi.mocked(api.fetchJobFromUrl).mockResolvedValue({
content: "Software Engineer role at Acme Corp",
url: "https://example.com/job",
});
vi.mocked(api.inferManualJob).mockResolvedValue({
job: {
title: "Software Engineer",
employer: "Acme Corp",
location: "Remote",
jobDescription: "Great opportunity to join our team.",
},
});
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
// Enter a URL
fireEvent.change(
screen.getByPlaceholderText("https://example.com/job-posting"),
{ target: { value: "https://example.com/job" } },
);
// Click Fetch
fireEvent.click(screen.getByRole("button", { name: /fetch/i }));
// Should show loading state then review
await screen.findByPlaceholderText("e.g. Junior Backend Engineer");
expect(api.fetchJobFromUrl).toHaveBeenCalledWith({
url: "https://example.com/job",
});
expect(api.inferManualJob).toHaveBeenCalledWith({
jobDescription: "Software Engineer role at Acme Corp",
});
// Check inferred values are shown
expect(
screen.getByPlaceholderText("e.g. Junior Backend Engineer"),
).toHaveValue("Software Engineer");
expect(screen.getByPlaceholderText("e.g. Acme Labs")).toHaveValue(
"Acme Corp",
);
});
it("preserves fetched URL in the job URL field", async () => {
vi.mocked(api.fetchJobFromUrl).mockResolvedValue({
content: "Job description content",
url: "https://example.com/job",
});
vi.mocked(api.inferManualJob).mockResolvedValue({
job: {
title: "Engineer",
employer: "Company",
},
});
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
fireEvent.change(
screen.getByPlaceholderText("https://example.com/job-posting"),
{ target: { value: "https://example.com/job" } },
);
fireEvent.click(screen.getByRole("button", { name: /fetch/i }));
await screen.findByPlaceholderText("e.g. Junior Backend Engineer");
// Check the job URL field has the fetched URL (first https://... input is Job URL)
const urlInputs = screen.getAllByPlaceholderText("https://...");
expect(urlInputs[0]).toHaveValue("https://example.com/job");
});
it("shows error and returns to paste step when fetch fails", async () => {
vi.mocked(api.fetchJobFromUrl).mockRejectedValue(
new Error("Failed to fetch URL"),
);
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
fireEvent.change(
screen.getByPlaceholderText("https://example.com/job-posting"),
{ target: { value: "https://example.com/bad-url" } },
);
fireEvent.click(screen.getByRole("button", { name: /fetch/i }));
await screen.findByText("Failed to fetch URL");
// Should still be on paste step
expect(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
).toBeInTheDocument();
});
it("shows error when inference fails after fetch", async () => {
vi.mocked(api.fetchJobFromUrl).mockResolvedValue({
content: "Job content",
url: "https://example.com/job",
});
vi.mocked(api.inferManualJob).mockRejectedValue(
new Error("Inference failed"),
);
render(
<ManualImportSheet open onOpenChange={vi.fn()} onImported={vi.fn()} />,
);
fireEvent.change(
screen.getByPlaceholderText("https://example.com/job-posting"),
{ target: { value: "https://example.com/job" } },
);
fireEvent.click(screen.getByRole("button", { name: /fetch/i }));
await screen.findByText("Inference failed");
// Should be back on paste step
expect(
screen.getByPlaceholderText(
"Paste the full job description here, or enter a URL above to fetch it...",
),
).toBeInTheDocument();
});
});
});