Jobber/orchestrator/src/client/components/JobDetailsEditDrawer.test.tsx
Shaheer Sarfaraz 3640abef2d
Migration to tanstack query (#199)
* commit at some point in the middle, WIP

* formatting

* ci passing

* comments

* handle no jobid case

* better error handling

* comments

* Update orchestrator/src/client/hooks/queries/useJobMutations.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update orchestrator/src/client/hooks/queries/useSettingsMutation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* better types

* formatter

* tracking inbox page

* in progress page

* tracer links page

* invalidate harder

* ensure tracer links docs show

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 23:04:47 +00:00

190 lines
5.7 KiB
TypeScript

import { createJob } from "@shared/testing/factories.js";
import type { Job } from "@shared/types.js";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../api";
import { _resetTracerReadinessCache } from "../hooks/useTracerReadiness";
import { renderWithQueryClient } from "../test/renderWithQueryClient";
import { JobDetailsEditDrawer } from "./JobDetailsEditDrawer";
const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
renderWithQueryClient(ui);
vi.mock("@/components/ui/sheet", () => ({
Sheet: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
open ? <div>{children}</div> : null,
SheetContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SheetHeader: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SheetTitle: ({ children }: { children: React.ReactNode }) => (
<h2>{children}</h2>
),
SheetDescription: ({ children }: { children: React.ReactNode }) => (
<p>{children}</p>
),
}));
vi.mock("../api", () => ({
updateJob: vi.fn(),
checkSponsor: vi.fn(),
rescoreJob: vi.fn(),
getTracerReadiness: vi.fn(),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe("JobDetailsEditDrawer", () => {
beforeEach(() => {
vi.clearAllMocks();
_resetTracerReadinessCache();
vi.mocked(api.getTracerReadiness).mockResolvedValue({
status: "ready",
canEnable: true,
publicBaseUrl: "https://my-jobops.example.com",
healthUrl: "https://my-jobops.example.com/health",
checkedAt: Date.now(),
lastSuccessAt: Date.now(),
reason: null,
});
});
it("saves details and reruns sponsor check when employer changes", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const onOpenChange = vi.fn();
vi.mocked(api.updateJob).mockResolvedValue({} as Job);
vi.mocked(api.checkSponsor).mockResolvedValue({} as Job);
render(
<JobDetailsEditDrawer
open
onOpenChange={onOpenChange}
job={createJob()}
onJobUpdated={onJobUpdated}
/>,
);
fireEvent.change(screen.getByLabelText("Employer *"), {
target: { value: "NewCo" },
});
fireEvent.click(screen.getByRole("button", { name: /save details/i }));
await waitFor(() =>
expect(api.updateJob).toHaveBeenCalledWith(
"job-1",
expect.objectContaining({
employer: "NewCo",
title: "Backend Engineer",
}),
),
);
expect(api.checkSponsor).toHaveBeenCalledWith("job-1");
expect(onJobUpdated).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("validates required fields before saving", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const onOpenChange = vi.fn();
render(
<JobDetailsEditDrawer
open
onOpenChange={onOpenChange}
job={createJob()}
onJobUpdated={onJobUpdated}
/>,
);
fireEvent.change(screen.getByLabelText("Title *"), {
target: { value: " " },
});
fireEvent.click(screen.getByRole("button", { name: /save details/i }));
expect(await screen.findByText("Title is required.")).toBeInTheDocument();
expect(api.updateJob).not.toHaveBeenCalled();
expect(onJobUpdated).not.toHaveBeenCalled();
});
it("offers a rescore action after successful save", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const onOpenChange = vi.fn();
const { toast } = await import("sonner");
vi.mocked(api.updateJob).mockResolvedValue({} as Job);
vi.mocked(api.rescoreJob).mockResolvedValue({} as Job);
render(
<JobDetailsEditDrawer
open
onOpenChange={onOpenChange}
job={createJob()}
onJobUpdated={onJobUpdated}
/>,
);
fireEvent.change(screen.getByLabelText("Salary"), {
target: { value: "GBP 90k" },
});
fireEvent.click(screen.getByRole("button", { name: /save details/i }));
await waitFor(() =>
expect(vi.mocked(toast.success)).toHaveBeenCalledWith(
"Job details updated",
expect.any(Object),
),
);
const successCalls = vi.mocked(toast.success).mock.calls;
const [, payload] =
successCalls.find((call) => call[0] === "Job details updated") ?? [];
expect(payload).toBeTruthy();
(payload as { action?: { onClick?: () => void } }).action?.onClick?.();
await waitFor(() => expect(api.rescoreJob).toHaveBeenCalledWith("job-1"));
expect(onJobUpdated).toHaveBeenCalledTimes(2);
});
it("persists tracer-links toggle with job updates", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const onOpenChange = vi.fn();
vi.mocked(api.updateJob).mockResolvedValue({} as Job);
render(
<JobDetailsEditDrawer
open
onOpenChange={onOpenChange}
job={createJob({ tracerLinksEnabled: false })}
onJobUpdated={onJobUpdated}
/>,
);
await waitFor(() => expect(api.getTracerReadiness).toHaveBeenCalled());
const tracerToggle = await screen.findByRole("checkbox", {
name: "Enable tracer links for this job",
});
await waitFor(() => expect(tracerToggle).toBeEnabled());
fireEvent.click(tracerToggle);
fireEvent.click(screen.getByRole("button", { name: /save details/i }));
await waitFor(() =>
expect(api.updateJob).toHaveBeenCalledWith(
"job-1",
expect.objectContaining({
tracerLinksEnabled: true,
}),
),
);
});
});