128 lines
7.3 KiB
TypeScript
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");
|
|
});
|