Files
jobtrackingapp/job-tracker-ui/src/quick-command-preservation.test.tsx
T
2026-09-28 18:19:27 +02:00

128 lines
7.3 KiB
TypeScript

import React from "react";
import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
import QuickCommandDialog from "./components/QuickCommandDialog";
const mockedApi = api as jest.Mocked<typeof api>;
beforeEach(() => {
jest.clearAllMocks();
localStorage.removeItem("uiLanguage");
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies"
? [{ id: 7, name: "Nordlys AS" }]
: { items: [{ id: 42, jobTitle: "Platform engineer", company: { name: "Nordlys AS" } }] } } as never));
});
test.each([
["Nordlys AS - Platform engineer", "/jobs/42"],
["Nordlys AS", "/companies?edit=7"],
])("[PRES-E07] command search opens %s at its existing destination", async (label, destination) => {
const onNavigate = jest.fn();
const onClose = jest.fn();
render(<I18nProvider><QuickCommandDialog open onClose={onClose} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
expect(screen.getByRole("dialog", { name: "Quick Search" })).toBeVisible();
fireEvent.change(screen.getByRole("textbox", { name: "Quick Search" }), { target: { value: "Nordlys" } });
fireEvent.click(await screen.findByText(label, { exact: true }));
expect(onNavigate).toHaveBeenCalledWith(destination);
expect(onClose).toHaveBeenCalledTimes(1);
expect(mockedApi.post).not.toHaveBeenCalled();
});
test("[PRES-E07 PRES-J04] the capture command opens review without creating a job, and closing clears search", async () => {
const onOpenAddJob = jest.fn();
const props = { onClose: jest.fn(), onNavigate: jest.fn(), onOpenAddJob };
const view = (open: boolean) => <I18nProvider><QuickCommandDialog open={open} {...props} /></I18nProvider>;
const { rerender } = render(view(true));
fireEvent.click(screen.getByRole("button", { name: /add new job/i }));
expect(onOpenAddJob).toHaveBeenCalledTimes(1);
expect(mockedApi.post).not.toHaveBeenCalled();
fireEvent.change(screen.getByRole("textbox"), { target: { value: "Nordlys" } });
await screen.findByText("Nordlys AS - Platform engineer");
rerender(view(false));
rerender(view(true));
await waitFor(() => expect(screen.getByRole("textbox")).toHaveValue(""));
expect(screen.queryByText("Nordlys AS - Platform engineer")).not.toBeInTheDocument();
});
test("[P5-00 PRES-E07] command and empty results use valid list semantics", async () => {
render(<I18nProvider><QuickCommandDialog open onClose={jest.fn()} onNavigate={jest.fn()} onOpenAddJob={jest.fn()} /></I18nProvider>);
expect(screen.getAllByRole("listitem")).toHaveLength(6);
expect(screen.getAllByRole("list").every(list => Array.from(list.children).every(item => item.tagName === "LI"))).toBe(true);
fireEvent.change(screen.getByRole("textbox"), { target: { value: "no-such-result" } });
mockedApi.get.mockResolvedValue({ data: [] } as never);
await screen.findByText("No matching commands or records.");
expect(screen.queryAllByRole("list").every(list => Array.from(list.children).every(item => item.tagName === "LI"))).toBe(true);
expect(mockedApi.post).not.toHaveBeenCalled();
});
test("[P5-02 PRES-E07] search failure is distinct from no matches and Retry recovers", async () => {
mockedApi.get.mockRejectedValueOnce(new Error("offline"));
render(<I18nProvider><QuickCommandDialog open onClose={jest.fn()} onNavigate={jest.fn()} onOpenAddJob={jest.fn()} /></I18nProvider>);
fireEvent.change(screen.getByRole("textbox", { name: "Quick Search" }), { target: { value: "Nordlys" } });
expect(await screen.findByRole("alert")).toHaveTextContent("Search could not be loaded");
expect(screen.queryByText("No matching commands or records.")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("Nordlys AS - Platform engineer")).toBeInTheDocument();
});
test("[P5-02 PRES-E09] closed and superseded searches cannot display stale account results", async () => {
let resolveOld!: (value: unknown) => void;
mockedApi.get.mockImplementation((url: string) => url === "/jobapplications"
? new Promise(resolve => { resolveOld = resolve; }) as never
: Promise.resolve({ data: [] }) as never);
const props = { onClose: jest.fn(), onNavigate: jest.fn(), onOpenAddJob: jest.fn() };
const view = (open: boolean) => <I18nProvider><QuickCommandDialog open={open} {...props} /></I18nProvider>;
const { rerender } = render(view(true));
fireEvent.change(screen.getByRole("textbox"), { target: { value: "old" } });
await waitFor(() => expect(resolveOld).toBeDefined());
rerender(view(false));
rerender(view(true));
await act(async () => resolveOld({ data: { items: [{ id: 42, jobTitle: "Account A job", company: { name: "Private" } }] } }));
expect(screen.queryByText(/Account A job/)).not.toBeInTheDocument();
expect(screen.getByRole("textbox")).toHaveValue("");
});
test("[P5-02 PRES-E07] a slower old query cannot replace the newer matching results", async () => {
let resolveOld!: (value: unknown) => void;
mockedApi.get.mockImplementation((url: string, config?: any) => url === "/companies"
? Promise.resolve({ data: [] }) as never
: config?.params?.q === "old"
? new Promise(resolve => { resolveOld = resolve; }) as never
: Promise.resolve({ data: { items: [{ id: 9, jobTitle: "New result", company: { name: "Current" } }] } }) as never);
render(<I18nProvider><QuickCommandDialog open onClose={jest.fn()} onNavigate={jest.fn()} onOpenAddJob={jest.fn()} /></I18nProvider>);
const search = screen.getByRole("textbox", { name: "Quick Search" });
fireEvent.change(search, { target: { value: "old" } });
await waitFor(() => expect(resolveOld).toBeDefined());
fireEvent.change(search, { target: { value: "new" } });
expect(await screen.findByText("Current - New result")).toBeInTheDocument();
await act(async () => resolveOld({ data: { items: [{ id: 8, jobTitle: "Old result", company: { name: "Private" } }] } }));
expect(screen.queryByText("Private - Old result")).not.toBeInTheDocument();
});
test("[P5-02 PRES-E07] arrows, Home, End and Enter activate one existing command; composition does not", () => {
const onNavigate = jest.fn();
render(<I18nProvider><QuickCommandDialog open onClose={jest.fn()} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
const search = screen.getByRole("textbox", { name: "Quick Search" });
fireEvent.keyDown(search, { key: "End" });
fireEvent.keyDown(search, { key: "Enter", isComposing: true });
expect(onNavigate).not.toHaveBeenCalled();
fireEvent.keyDown(search, { key: "Home" });
fireEvent.keyDown(search, { key: "ArrowDown" });
fireEvent.keyDown(search, { key: "Enter" });
expect(onNavigate).toHaveBeenCalledWith("/jobs");
});
test("[P5-02 PRES-E07] arrowing from a focused result keeps focus and activation on the same item", () => {
const onNavigate = jest.fn();
render(<I18nProvider><QuickCommandDialog open onClose={jest.fn()} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
const dashboard = screen.getByRole("button", { name: /go to dashboard/i });
act(() => dashboard.focus());
fireEvent.keyDown(dashboard, { key: "ArrowDown" });
const jobs = screen.getByRole("button", { name: /go to jobs/i });
expect(jobs).toHaveFocus();
fireEvent.click(jobs);
expect(onNavigate).toHaveBeenCalledWith("/jobs");
});