Files
jobtrackingapp/job-tracker-ui/src/job-discovery.test.tsx
T

158 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { api } from "./api";
import { ConfirmProvider } from "./confirm";
import { PromptProvider } from "./prompt";
import { I18nProvider } from "./i18n/I18nProvider";
import JobDiscoveryPage from "./views/JobDiscoveryPage";
jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn(), patch: jest.fn(), delete: jest.fn() } }));
const mockedApi = api as jest.Mocked<typeof api>;
function renderPage() {
return render(<I18nProvider><ConfirmProvider><PromptProvider><MemoryRouter><JobDiscoveryPage /></MemoryRouter></PromptProvider></ConfirmProvider></I18nProvider>);
}
function searchResponse(data: any[]) {
mockedApi.get.mockImplementation((url) => Promise.resolve({ data: String(url).endsWith("saved-searches") ? [] : data } as any));
}
beforeEach(() => { window.localStorage.clear(); searchResponse([]); });
afterEach(() => jest.clearAllMocks());
test("shows honest source metadata and offers the existing reviewed save flow", async () => {
searchResponse([{ id: "abc", title: "Backend Developer", company: "Acme", location: "OSLO", modifiedAt: "2026-08-01T10:00:00Z", deadline: "2026-08-15T23:59:59Z", url: "https://arbeidsplassen.nav.no/stillinger/stilling/abc", source: "nav", sourceName: "NAV Arbeidsplassen", acquisitionType: "searched", retrievedAt: "2026-08-10T10:00:00Z", countryCode: "NO" }]);
renderPage();
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: "backend" } });
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
expect(await screen.findByText("Backend Developer")).toBeInTheDocument();
expect(screen.getByText("NAV Arbeidsplassen")).toBeInTheDocument();
expect(screen.getByText(/Searched listing · Retrieved/)).toBeInTheDocument();
expect(screen.getByText(/Application deadline:/)).toBeInTheDocument();
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/job-discovery/search", { params: { q: "backend", location: undefined } }));
expect(screen.getByRole("button", { name: "Save to tracker" })).toBeInTheDocument();
});
test("distinguishes the initial guidance from a completed empty search", async () => {
renderPage();
expect(screen.getByText("Search recent active vacancies by role, company, or municipality.")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
expect(await screen.findByText(/No active vacancies matched this search/)).toBeInTheDocument();
expect(screen.queryByText("Search recent active vacancies by role, company, or municipality.")).not.toBeInTheDocument();
});
test("retries the last submitted search after an error", async () => {
let searchAttempts = 0;
mockedApi.get.mockImplementation((url) => {
if (String(url).endsWith("saved-searches")) return Promise.resolve({ data: [] } as any);
searchAttempts += 1;
return searchAttempts === 1 ? Promise.reject(new Error("offline")) : Promise.resolve({ data: [] } as any);
});
renderPage();
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: " utvikler " } });
fireEvent.change(screen.getByLabelText("Municipality"), { target: { value: " Oslo " } });
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
fireEvent.click(await screen.findByRole("button", { name: "Retry" }));
await waitFor(() => expect(searchAttempts).toBe(2));
expect(mockedApi.get).toHaveBeenLastCalledWith("/job-discovery/search", { params: { q: "utvikler", location: "Oslo" } });
});
test("sorts returned vacancies by title", async () => {
searchResponse([
{ id: "2", title: "Zoologist", url: "https://arbeidsplassen.nav.no/2", source: "nav", countryCode: "NO" },
{ id: "1", title: "Analyst", url: "https://arbeidsplassen.nav.no/1", source: "nav", countryCode: "NO" },
]);
renderPage();
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
await screen.findByText("2 vacancies found");
fireEvent.mouseDown(screen.getByRole("combobox", { name: "Sort results" }));
fireEvent.click(await screen.findByRole("option", { name: "Title A–Z" }));
expect(screen.getAllByRole("heading", { level: 6 }).map((element) => element.textContent)).toEqual(["Analyst", "Zoologist"]);
});
test("renders the discovery workflow in Norwegian Bokmål", async () => {
window.localStorage.setItem("uiLanguage", "nb");
renderPage();
expect(screen.getByLabelText("Rolle eller bedrift")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Søk i NAV" }));
expect(await screen.findByText(/Ingen aktive stillinger samsvarte/)).toBeInTheDocument();
});
test("runs a saved search, marks unseen vacancies, and dismisses them", async () => {
const saved = { id: 7, name: "Backend Oslo", query: "backend", location: "Oslo", isActive: true, resultCount: 0, dismissedCount: 0 };
mockedApi.get.mockResolvedValue({ data: [saved] } as any);
mockedApi.post.mockResolvedValue({ data: { search: { ...saved, resultCount: 1 }, jobs: [{ job: { id: "new-1", title: "Platform Engineer", url: "https://example.test/new-1", source: "nav", countryCode: "NO" }, isNew: true, isDismissed: false }] } } as any);
mockedApi.patch.mockResolvedValue({ data: null } as any);
renderPage();
fireEvent.click(await screen.findByRole("button", { name: "Check for new jobs" }));
expect(await screen.findByText("Platform Engineer")).toBeInTheDocument();
expect(screen.getByText("New")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
await waitFor(() => expect(screen.queryByText("Platform Engineer")).not.toBeInTheDocument());
expect(mockedApi.patch).toHaveBeenCalledWith("/job-discovery/saved-searches/7/results/new-1/dismiss", { isDismissed: true });
});
test("lets the user pause automatic alerts for a saved search", async () => {
const saved = { id: 7, name: "Backend Oslo", query: "backend", location: "Oslo", isActive: true, resultCount: 1, dismissedCount: 0 };
mockedApi.get.mockResolvedValue({ data: [saved] } as any);
mockedApi.patch.mockResolvedValue({ data: { ...saved, isActive: false } } as any);
renderPage();
const toggle = await screen.findByRole("switch", { name: "Automatic alerts" });
expect(toggle).toBeChecked();
fireEvent.click(toggle);
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/job-discovery/saved-searches/7", { isActive: false }));
await waitFor(() => expect(toggle).not.toBeChecked());
});
test("announces saved-search loading without presenting a successful empty list", async () => {
let finish!: (value: any) => void;
mockedApi.get.mockReturnValue(new Promise((resolve) => { finish = resolve; }));
renderPage();
expect(screen.getByRole("status", { name: "Loading saved searches…" })).toBeInTheDocument();
expect(screen.queryByText("No saved searches yet.")).not.toBeInTheDocument();
finish({ data: [] });
expect(await screen.findByText("No saved searches yet.")).toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading saved searches…" })).not.toBeInTheDocument();
});
test("reports saved-search failure and retries that read without replacing active discovery filters or results", async () => {
let savedAttempts = 0;
mockedApi.get.mockImplementation((url) => {
if (String(url).endsWith("saved-searches")) {
savedAttempts++;
return savedAttempts === 1
? Promise.reject({ response: { status: 500, data: { detail: "Internal database stack trace" } } })
: Promise.resolve({ data: [{ id: 7, name: "Backend Oslo", query: "backend", location: "Oslo", isActive: true, resultCount: 0, dismissedCount: 0 }] } as any);
}
return Promise.resolve({ data: [{ id: "vacancy", title: "Existing result", url: "https://example.test/vacancy", source: "nav", countryCode: "NO" }] } as any);
});
renderPage();
expect(await screen.findByText("Saved searches could not be loaded. Try again.")).toBeInTheDocument();
expect(screen.queryByText("No saved searches yet.")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Save search" })).toBeDisabled();
expect(screen.queryByText(/Internal database stack trace/)).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: "developer" } });
fireEvent.change(screen.getByLabelText("Municipality"), { target: { value: "Bergen" } });
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
await screen.findByText("Existing result");
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("Backend Oslo")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Save search" })).toBeEnabled();
expect(screen.getByLabelText("Role or company")).toHaveValue("developer");
expect(screen.getByLabelText("Municipality")).toHaveValue("Bergen");
expect(screen.getByText("Existing result")).toBeInTheDocument();
expect(screen.queryByText("Saved searches could not be loaded. Try again.")).not.toBeInTheDocument();
});
test.each([null, { message: "not a list" }, [{ id: 7 }]])("rejects malformed saved-search payload %j as a failed read rather than an empty list", async (data) => {
mockedApi.get.mockResolvedValue({ data } as any);
renderPage();
expect(await screen.findByText("Saved searches could not be loaded. Try again.")).toBeInTheDocument();
expect(screen.queryByText("No saved searches yet.")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument();
});