162 lines
8.1 KiB
TypeScript
162 lines
8.1 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
import { translations } from "./i18n/translations";
|
|
import ProfileCompleteness, { CareerVersionView } from "./views/career/ProfileCompleteness";
|
|
|
|
type HistoryProps = React.ComponentProps<typeof ProfileCompleteness> & {
|
|
versionsLoading?: boolean;
|
|
versionsError?: string | null;
|
|
onRetryVersions?: () => void;
|
|
restoreDisabled?: boolean;
|
|
restoreError?: string | null;
|
|
};
|
|
type HistoryKey = "careerHistoryLoading" | "careerHistoryLoadFailed" | "careerHistoryEmpty"
|
|
| "careerSaveBeforeRestore" | "careerRestoreVersion" | "careerRestoreFailed";
|
|
const en = translations.en as typeof translations.en & Record<HistoryKey, string>;
|
|
const nb = translations.nb as typeof translations.nb & Record<HistoryKey, string>;
|
|
const versions: CareerVersionView[] = [
|
|
{ version: 4, source: "manual", createdAtUtc: "2026-10-12T01:00:00Z", isCurrent: true },
|
|
{ version: 3, source: "import", createdAtUtc: "2026-10-11T01:00:00Z", isCurrent: false },
|
|
];
|
|
|
|
function historyTitle(count: number, language: "en" | "nb" = "en") {
|
|
return translations[language].profileCvVersionHistory.replace("{count}", String(count));
|
|
}
|
|
|
|
function view(props: Partial<HistoryProps> = {}) {
|
|
const defaults: HistoryProps = {
|
|
completeness: null, versions, loading: false, onRestore: jest.fn(), showSummary: false,
|
|
};
|
|
return <I18nProvider><ProfileCompleteness {...defaults} {...props} /></I18nProvider>;
|
|
}
|
|
|
|
function expand(count = 2) {
|
|
fireEvent.click(screen.getByRole("button", { name: historyTitle(count) }));
|
|
}
|
|
|
|
beforeEach(() => window.localStorage.removeItem("uiLanguage"));
|
|
afterEach(() => window.localStorage.removeItem("uiLanguage"));
|
|
|
|
test("the first history read announces progress without claiming an empty history", () => {
|
|
const restore = jest.fn();
|
|
render(view({ versions: [], versionsLoading: true, onRestore: restore }));
|
|
expect(screen.getByRole("progressbar", { name: en.careerHistoryLoading })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: historyTitle(0), level: 2 })).toBeInTheDocument();
|
|
expect(screen.queryByText(en.careerHistoryEmpty)).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /^Restore/ })).not.toBeInTheDocument();
|
|
expect(restore).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("a failed history read retains known rows and retries only through its history owner", () => {
|
|
const retry = jest.fn();
|
|
const restore = jest.fn();
|
|
const rendered = render(view({ versionsError: "History service unavailable", onRetryVersions: retry, onRestore: restore }));
|
|
expand();
|
|
const failure = screen.getByRole("alert");
|
|
expect(failure).toHaveTextContent(en.careerHistoryLoadFailed);
|
|
expect(failure).toHaveTextContent("History service unavailable");
|
|
expect(screen.getByText(/^v4 · manual ·/)).toBeVisible();
|
|
expect(screen.getByText(/^v3 · import ·/)).toBeVisible();
|
|
const retryControl = within(failure).getByRole("button", { name: en.retry });
|
|
retryControl.focus(); fireEvent.click(retryControl);
|
|
expect(screen.getByRole("group", { name: historyTitle(2) })).toHaveFocus();
|
|
expect(retry).toHaveBeenCalledTimes(1);
|
|
expect(restore).not.toHaveBeenCalled();
|
|
|
|
rendered.rerender(view({ versionsLoading: true, versionsError: "History service unavailable", onRetryVersions: retry, onRestore: restore }));
|
|
expect(screen.getByRole("alert")).toHaveTextContent("History service unavailable");
|
|
expect(screen.getByRole("button", { name: en.retry })).toBeDisabled();
|
|
expect(screen.getByRole("progressbar", { name: en.careerHistoryLoading })).toBeInTheDocument();
|
|
expect(screen.getByText(/^v3 · import ·/)).toBeVisible();
|
|
expect(retry).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("a settled zero-version history is a named empty section rather than a hidden capability", () => {
|
|
render(view({ versions: [] }));
|
|
const history = screen.getByRole("region", { name: historyTitle(0) });
|
|
expect(within(history).getByRole("heading", { name: historyTitle(0), level: 2 })).toBeInTheDocument();
|
|
expect(within(history).getByText(en.careerHistoryEmpty)).toBeVisible();
|
|
expect(within(history).queryByRole("progressbar")).not.toBeInTheDocument();
|
|
expect(within(history).queryByRole("button", { name: /^Restore/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("one current version remains visible in Bokmål without offering a restore of the current source", () => {
|
|
window.localStorage.setItem("uiLanguage", "nb");
|
|
render(view({ versions: [versions[0]] }));
|
|
const row = screen.getByText(/^v4 · manual ·/);
|
|
expect(row).toBeVisible();
|
|
expect(row).toHaveTextContent(nb.profileCvVersionCurrent);
|
|
expect(screen.getByRole("region", { name: historyTitle(1, "nb") })).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /^Gjenopprett/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("the collapsed history keeps twelve rows and contextual restore actions pass the exact selected version", () => {
|
|
const restore = jest.fn();
|
|
const many = Array.from({ length: 13 }, (_, index) => ({
|
|
version: 13 - index, source: "manual", createdAtUtc: "2026-10-12T01:00:00Z", isCurrent: index === 0,
|
|
}));
|
|
render(view({ versions: many, onRestore: restore }));
|
|
const disclosure = screen.getByRole("button", { name: historyTitle(13) });
|
|
expect(disclosure).toHaveAttribute("aria-expanded", "false");
|
|
expect(screen.queryByRole("button", { name: /^Restore v/ })).not.toBeInTheDocument();
|
|
fireEvent.click(disclosure);
|
|
expect(disclosure).toHaveAttribute("aria-expanded", "true");
|
|
expect(screen.getAllByText(/^v\d+ · manual ·/)).toHaveLength(12);
|
|
expect(screen.queryByText(/^v1 · manual ·/)).not.toBeInTheDocument();
|
|
expect(screen.getAllByRole("button", { name: /^Restore v/ })).toHaveLength(11);
|
|
expect(screen.queryByRole("button", { name: "Restore v13" })).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "Restore v12" }));
|
|
expect(restore).toHaveBeenCalledTimes(1);
|
|
expect(restore).toHaveBeenCalledWith(12);
|
|
});
|
|
|
|
test("dirty source state explains the restore guard and re-enables the same version after the parent clears it", () => {
|
|
const restore = jest.fn();
|
|
const rendered = render(view({ restoreDisabled: true, onRestore: restore }));
|
|
expand();
|
|
const guarded = screen.getByRole("button", { name: /^Restore/ });
|
|
expect(guarded).toBeDisabled();
|
|
expect(screen.getByText(en.careerSaveBeforeRestore)).toBeVisible();
|
|
fireEvent.click(guarded);
|
|
expect(restore).not.toHaveBeenCalled();
|
|
|
|
rendered.rerender(view({ restoreDisabled: false, onRestore: restore }));
|
|
expect(screen.queryByText(en.careerSaveBeforeRestore)).not.toBeInTheDocument();
|
|
const available = screen.getByRole("button", { name: "Restore v3" });
|
|
expect(available).toBeEnabled();
|
|
fireEvent.click(available);
|
|
expect(restore).toHaveBeenCalledWith(3);
|
|
expect(restore).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("the existing overall loading guard still blocks history restores", () => {
|
|
const restore = jest.fn();
|
|
render(view({ loading: true, onRestore: restore }));
|
|
expand();
|
|
const action = screen.getByRole("button", { name: /^Restore/ });
|
|
expect(action).toBeDisabled();
|
|
fireEvent.click(action);
|
|
expect(restore).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("restore failure stays visible with the known history until the parent clears it and retry is explicit", () => {
|
|
const restore = jest.fn();
|
|
const rendered = render(view({ restoreError: "Restore service unavailable", onRestore: restore }));
|
|
expand();
|
|
expect(screen.getByRole("alert")).toHaveTextContent(en.careerRestoreFailed);
|
|
expect(screen.getByRole("alert")).toHaveTextContent("Restore service unavailable");
|
|
expect(screen.getByText(/^v3 · import ·/)).toBeVisible();
|
|
expect(restore).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole("button", { name: "Restore v3" }));
|
|
expect(restore).toHaveBeenCalledTimes(1);
|
|
expect(restore).toHaveBeenCalledWith(3);
|
|
expect(screen.getByRole("alert")).toBeInTheDocument();
|
|
|
|
rendered.rerender(view({ restoreError: null, onRestore: restore }));
|
|
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
expect(screen.getByText(/^v4 · manual ·/)).toBeVisible();
|
|
expect(restore).toHaveBeenCalledTimes(1);
|
|
});
|