377 lines
24 KiB
TypeScript
377 lines
24 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
import { api } from "./api";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
import { translations } from "./i18n/translations";
|
|
import { emptyStructuredCv, StructuredCvProfile } from "./profileCv";
|
|
import { ToastProvider } from "./toast";
|
|
import CareerProfilePage from "./views/CareerProfilePage";
|
|
|
|
jest.mock("./api", () => ({
|
|
getApiErrorMessage: jest.requireActual("./api").getApiErrorMessage,
|
|
api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn() },
|
|
}));
|
|
jest.mock("./components/CropImageDialog", () => () => null);
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
const en = translations.en;
|
|
const nb = translations.nb;
|
|
const rawText = "Original stored source\nNo silent import acceptance";
|
|
let profile: StructuredCvProfile;
|
|
let provider: "local" | "external";
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (reason: unknown) => void;
|
|
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function sourceResponse() {
|
|
return { data: { revision: 7, profile, cvText: rawText, completeness: { percent: 72, missing: [], sections: [] } } };
|
|
}
|
|
|
|
function renderPage() {
|
|
return render(<ToastProvider><I18nProvider><CareerProfilePage /></I18nProvider></ToastProvider>);
|
|
}
|
|
|
|
async function ready() {
|
|
await waitFor(() => expect(screen.getByLabelText(en.profileCvContactFullName)).toHaveValue("Recorded owner"));
|
|
await waitFor(() => expect(screen.getByRole("button", { name: en.profileSaveChanges })).toBeEnabled());
|
|
}
|
|
|
|
async function changeLocale() {
|
|
await act(async () => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })));
|
|
}
|
|
|
|
function readCount(url: string) {
|
|
return mockedApi.get.mock.calls.filter(([requested]) => requested === url).length;
|
|
}
|
|
|
|
function editor() {
|
|
const element = document.getElementById("career-profile-editor");
|
|
expect(element).not.toBeNull();
|
|
return within(element!);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
window.localStorage.clear();
|
|
window.history.replaceState({}, "", "/career?section=profile&retained=1#editor");
|
|
provider = "local";
|
|
profile = {
|
|
...emptyStructuredCv(),
|
|
metadata: { profileVersion: 4, fields: {} },
|
|
contact: {
|
|
fullName: "Recorded owner", headline: "Backend engineer", email: "career@example.test",
|
|
phone: "+47 12345678", location: "Oslo", website: "https://example.test",
|
|
linkedIn: "https://example.test/linkedin", gitHub: "https://example.test/github",
|
|
links: [{ label: "Portfolio", url: "https://example.test/portfolio" }],
|
|
},
|
|
summary: ["Recorded summary"], skills: ["C#", "SQL"], interests: ["Walking"],
|
|
awards: ["Recorded award"], publications: ["Recorded publication"],
|
|
organisations: ["Recorded organisation"], references: ["Recorded reference"],
|
|
jobs: [{ id: "opaque-job-key", title: "Recorded role", company: "Recorded company", location: "Oslo", start: "2020", end: "Current", isCurrent: true, bullets: ["Recorded outcome"], skills: ["C#"] }],
|
|
education: [{ id: "opaque-education-key", qualification: "Recorded degree", qualificationLevel: "Master", institution: "Recorded school", location: "Oslo", start: "2016", end: "2019", details: ["Recorded distinction"] }],
|
|
certifications: [{ id: "opaque-certification-key", name: "Recorded certification", issuer: "Recorded issuer", date: "2024", details: ["Recorded credential detail"] }],
|
|
projects: [{ id: "opaque-project-key", name: "Recorded project", role: "Owner", start: "2023", end: "2024", bullets: ["Recorded project outcome"], skills: ["SQL"] }],
|
|
languages: [{ name: "English", level: "Professional", notes: "Recorded language note" }],
|
|
otherSections: [{ title: "Volunteering", items: ["Recorded community work"] }],
|
|
sections: [{ name: "Original source", content: "No silent import acceptance", wordCount: 5 }],
|
|
};
|
|
mockedApi.get.mockImplementation((url: string) => {
|
|
if (url === "/career/profile") return Promise.resolve(sourceResponse() as any);
|
|
if (url === "/auth/me") return Promise.resolve({ data: { id: "editor-owner", provider, email: "account@example.test", displayName: "Recorded owner" } } as any);
|
|
if (url === "/career/profile/versions") return Promise.resolve({ data: [
|
|
{ version: 4, source: "manual", createdAtUtc: "2026-10-12T01:00:00Z", isCurrent: true },
|
|
{ version: 3, source: "manual", createdAtUtc: "2026-10-11T01:00:00Z", isCurrent: false },
|
|
] } as any);
|
|
if (url === "/profile-cv/runs") return Promise.resolve({ data: [] } as any);
|
|
return Promise.resolve({ data: { revision: 8 } } as any);
|
|
});
|
|
mockedApi.put.mockResolvedValue({ data: { revision: 8 } } as any);
|
|
mockedApi.post.mockResolvedValue({ data: { revision: 8 } } as any);
|
|
});
|
|
|
|
test("a locale update retains dirty structured and raw drafts without reading their source again", async () => {
|
|
renderPage();
|
|
await ready();
|
|
fireEvent.change(screen.getByLabelText(en.profileCvContactFullName), { target: { value: "Unsaved canonical owner" } });
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceImportTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(en.profileCvRawPanelTitle) }));
|
|
fireEvent.change(screen.getByLabelText(en.profileCvTextLabel), { target: { value: "Unsaved raw source" } });
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceProfileTab }));
|
|
const before = [readCount("/career/profile"), readCount("/auth/me"), readCount("/career/profile/versions"), readCount("/profile-cv/runs")];
|
|
|
|
await changeLocale();
|
|
expect(screen.getByLabelText(nb.profileCvContactFullName)).toHaveValue("Unsaved canonical owner");
|
|
expect([readCount("/career/profile"), readCount("/auth/me"), readCount("/career/profile/versions"), readCount("/profile-cv/runs")]).toEqual(before);
|
|
fireEvent.click(screen.getByRole("tab", { name: nb.careerWorkspaceImportTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(nb.profileCvRawPanelTitle) }));
|
|
expect(screen.getByLabelText(nb.profileCvTextLabel)).toHaveValue("Unsaved raw source");
|
|
expect(window.location.search).toContain("retained=1");
|
|
expect(window.location.hash).toBe("#editor");
|
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test.each([
|
|
['en', 'Projects', 'Project name', 'Certifications', 'Certification name'],
|
|
['nb', 'Prosjekter', 'Prosjektnavn', 'Sertifiseringer', 'Navn på sertifisering'],
|
|
])('edits %s canonical projects and certifications through the existing versioned Save without changing their IDs or sibling facts', async (language, projectsName, projectLabel, certificationsName, certificationLabel) => {
|
|
localStorage.setItem('uiLanguage', language);
|
|
const before = JSON.parse(JSON.stringify(profile)) as StructuredCvProfile;
|
|
renderPage();
|
|
const projects = await screen.findByRole('region', { name: projectsName });
|
|
const certifications = screen.getByRole('region', { name: certificationsName });
|
|
const projectField = await within(projects).findByRole('textbox', { name: projectLabel });
|
|
await waitFor(() => expect(projectField).toBeEnabled());
|
|
expect(projectField).toHaveValue('Recorded project');
|
|
fireEvent.change(projectField, { target: { value: 'Reviewed community project' } });
|
|
fireEvent.change(within(certifications).getByRole('textbox', { name: certificationLabel }), { target: { value: 'Reviewed credential' } });
|
|
fireEvent.click(screen.getByRole('button', { name: language === 'nb' ? nb.profileSaveChanges : en.profileSaveChanges }));
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1));
|
|
const payload = mockedApi.put.mock.calls[0][1] as { profile: StructuredCvProfile; expectedVersion: number; cvText: string };
|
|
expect(payload.expectedVersion).toBe(7);
|
|
expect(payload.cvText).toBe(rawText);
|
|
expect(payload.profile.projects).toEqual([{ ...before.projects[0], name: 'Reviewed community project' }]);
|
|
expect(payload.profile.certifications).toEqual([{ ...before.certifications[0], name: 'Reviewed credential' }]);
|
|
expect(payload.profile.contact).toEqual(before.contact);
|
|
expect(payload.profile.jobs).toEqual(before.jobs);
|
|
expect(payload.profile.education).toEqual(before.education);
|
|
expect(payload.profile.metadata).toEqual(before.metadata);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('project and certification add/remove controls keep sibling entries and focus before the explicit canonical Save', async () => {
|
|
renderPage();
|
|
await ready();
|
|
const projects = screen.getByRole('region', { name: en.cvSectionProjects });
|
|
fireEvent.click(within(projects).getByRole('button', { name: en.careerProfileAddProject }));
|
|
fireEvent.change(within(projects).getAllByRole('textbox', { name: en.careerProfileProjectName })[1], { target: { value: 'Added project' } });
|
|
fireEvent.click(within(projects).getByRole('button', { name: en.careerProfileRemoveProject.replace('{index}', '1') }));
|
|
expect(within(projects).getByRole('button', { name: en.careerProfileAddProject })).toHaveFocus();
|
|
const certifications = screen.getByRole('region', { name: en.cvSectionCertifications });
|
|
fireEvent.click(within(certifications).getByRole('button', { name: en.careerProfileAddCertification }));
|
|
expect(within(certifications).getAllByRole('textbox', { name: en.careerProfileCertificationName })).toHaveLength(2);
|
|
fireEvent.click(within(certifications).getByRole('button', { name: en.careerProfileRemoveCertification.replace('{index}', '2') }));
|
|
expect(within(certifications).getByRole('button', { name: en.careerProfileAddCertification })).toHaveFocus();
|
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole('button', { name: en.profileSaveChanges }));
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1));
|
|
const payload = mockedApi.put.mock.calls[0][1] as { profile: StructuredCvProfile };
|
|
expect(payload.profile.projects.map(project => project.name)).toEqual(['Added project']);
|
|
expect(payload.profile.certifications).toEqual(profile.certifications);
|
|
});
|
|
|
|
test("a pending initial read keeps one request and reports its failure in the latest locale without language writes", async () => {
|
|
const pending = deferred<any>();
|
|
const read = mockedApi.get.getMockImplementation()!;
|
|
mockedApi.get.mockImplementation((url: string) => url === "/career/profile" ? pending.promise : read(url));
|
|
renderPage();
|
|
expect(screen.getByRole("button", { name: en.profileSaveChanges })).toBeDisabled();
|
|
expect(screen.queryByRole("button", { name: en.saving })).not.toBeInTheDocument();
|
|
await changeLocale();
|
|
await act(async () => { pending.reject({ isAxiosError: true, message: "Request failed with status code 500", response: { status: 500, data: { revision: 8 } } }); });
|
|
|
|
expect(readCount("/career/profile")).toBe(1);
|
|
expect(readCount("/auth/me")).toBe(1);
|
|
const failure = await editor().findByRole("alert");
|
|
expect(failure).toHaveTextContent(nb.profileLoadFailed);
|
|
expect(failure).not.toHaveTextContent("Request failed with status code 500");
|
|
expect(screen.queryByRole("button", { name: nb.saving })).not.toBeInTheDocument();
|
|
await act(async () => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "en" })));
|
|
expect(editor().getByRole("alert")).toHaveTextContent(en.profileLoadFailed);
|
|
expect(editor().getByRole("alert")).not.toHaveTextContent(nb.profileLoadFailed);
|
|
expect(readCount("/career/profile")).toBe(1);
|
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("a held Save locks every editor mutation, raw input and history restore until its snapshot settles", async () => {
|
|
const pending = deferred<any>();
|
|
mockedApi.put.mockReturnValue(pending.promise);
|
|
renderPage();
|
|
await ready();
|
|
fireEvent.change(screen.getByLabelText(en.profileCvContactFullName), { target: { value: "Snapshot owner" } });
|
|
fireEvent.click(screen.getByRole("button", { name: /Version history/ }));
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
try {
|
|
expect(mockedApi.put).toHaveBeenCalledTimes(1);
|
|
const snapshot = mockedApi.put.mock.calls[0][1] as { profile: StructuredCvProfile; cvText: string };
|
|
expect(snapshot.profile.contact.fullName).toBe("Snapshot owner");
|
|
expect(snapshot.cvText).toBe(rawText);
|
|
expect(screen.getAllByRole("textbox").filter((field) => !(field as HTMLInputElement).disabled).map((field) => field.getAttribute("aria-label") || field.id)).toEqual([]);
|
|
for (const name of [en.profileCvAddLink, en.profileCvStructuredAddLanguage, en.profileCvStructuredAddJob, en.profileCvStructuredAddEducation, en.profileCvStructuredAddOtherSection, en.careerProfileAddProject, en.careerProfileAddCertification, en.careerRestoreVersion.replace("{version}", "3")]) {
|
|
expect(screen.getByRole("button", { name })).toBeDisabled();
|
|
}
|
|
for (const button of screen.getAllByRole("button", { name: /^(Remove|Delete custom link|Bold|Italic|Underline|Link$)/ })) expect(button).toBeDisabled();
|
|
expect(screen.getByRole("button", { name: en.saving })).toBeDisabled();
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceImportTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(en.profileCvRawPanelTitle) }));
|
|
expect(screen.getByLabelText(en.profileCvTextLabel)).toBeDisabled();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
} finally {
|
|
await act(async () => { pending.resolve({ data: { revision: 8 } }); await pending.promise; });
|
|
}
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceProfileTab }));
|
|
expect(screen.getByLabelText(en.profileCvContactFullName)).toHaveValue("Snapshot owner");
|
|
expect(screen.getByLabelText(en.profileCvContactFullName)).toBeEnabled();
|
|
expect(screen.getByRole("button", { name: en.profileSaveChanges })).toBeEnabled();
|
|
expect(mockedApi.put).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("two Save activations before React commits pending state send only one canonical PUT", async () => {
|
|
const pending = deferred<any>();
|
|
mockedApi.put.mockReturnValue(pending.promise);
|
|
renderPage();
|
|
await ready();
|
|
const save = screen.getByRole("button", { name: en.profileSaveChanges });
|
|
try {
|
|
act(() => {
|
|
save.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
save.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
});
|
|
expect(mockedApi.put).toHaveBeenCalledTimes(1);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
} finally {
|
|
await act(async () => { pending.resolve({ data: { revision: 8 } }); await pending.promise; });
|
|
}
|
|
});
|
|
|
|
test("a failed Save retains a scoped localized error and draft for retry through the same Save button", async () => {
|
|
localStorage.setItem("uiLanguage", "nb");
|
|
mockedApi.put.mockRejectedValueOnce({ isAxiosError: true, message: "Request failed with status code 500", response: { status: 500, data: { revision: 8 } } });
|
|
renderPage();
|
|
await waitFor(() => expect(screen.getByLabelText(nb.profileCvContactFullName)).toHaveValue("Recorded owner"));
|
|
fireEvent.change(screen.getByLabelText(nb.profileCvContactFullName), { target: { value: "Beholdt utkast" } });
|
|
fireEvent.click(screen.getByRole("button", { name: nb.profileSaveChanges }));
|
|
|
|
const failure = await editor().findByRole("alert");
|
|
expect(failure).toHaveTextContent(nb.profileUpdateFailed);
|
|
expect(failure).not.toHaveTextContent("Request failed with status code 500");
|
|
expect(screen.getByLabelText(nb.profileCvContactFullName)).toHaveValue("Beholdt utkast");
|
|
expect(screen.getByText(nb.assetsUnsavedChanges)).toBeInTheDocument();
|
|
await act(async () => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "en" })));
|
|
expect(editor().getByRole("alert")).toHaveTextContent(en.profileUpdateFailed);
|
|
expect(editor().getByRole("alert")).not.toHaveTextContent(nb.profileUpdateFailed);
|
|
await changeLocale();
|
|
const reads = mockedApi.get.mock.calls.length;
|
|
const save = screen.getByRole("button", { name: nb.profileSaveChanges });
|
|
expect(save).toBeEnabled();
|
|
expect(mockedApi.put).toHaveBeenCalledTimes(1);
|
|
fireEvent.click(save);
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
|
expect(mockedApi.put.mock.calls[1]).toEqual(mockedApi.put.mock.calls[0]);
|
|
await waitFor(() => expect(editor().queryByRole("alert")).not.toBeInTheDocument());
|
|
expect(screen.getByLabelText(nb.profileCvContactFullName)).toHaveValue("Beholdt utkast");
|
|
expect(screen.queryByText(nb.assetsUnsavedChanges)).not.toBeInTheDocument();
|
|
expect(mockedApi.get).toHaveBeenCalledTimes(reads + 1);
|
|
expect(readCount("/career/profile/versions")).toBe(2);
|
|
expect(readCount("/career/profile")).toBe(1);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("the editor exposes named H2/H3 groups and contextual repeated-item remove controls", async () => {
|
|
renderPage();
|
|
await ready();
|
|
expect(screen.getByRole("heading", { name: en.profileCvStructuredEditor, level: 2 })).toBeInTheDocument();
|
|
expect(screen.getByRole("region", { name: en.profileCvStructuredEditor })).toBeInTheDocument();
|
|
for (const name of [en.careerProfileContactSection, en.profileCvStructuredLanguages, en.profileCvStructuredJobs, en.profileCvStructuredEducation, en.profileCvStructuredOtherSections]) {
|
|
expect(screen.getByRole("heading", { name, level: 3 })).toBeInTheDocument();
|
|
expect(screen.getByRole("region", { name })).toBeInTheDocument();
|
|
}
|
|
for (const name of [en.careerProfileRemoveJob, en.careerProfileRemoveEducation, en.careerProfileRemoveLanguage, en.careerProfileRemoveOtherSection]) {
|
|
expect(screen.getByRole("button", { name: name.replace("{index}", "1") })).toBeEnabled();
|
|
}
|
|
expect(screen.getByRole("button", { name: en.profileCvDeleteCustomLink.replace("{index}", "1") })).toBeEnabled();
|
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("manual edits preserve long-tail data, custom links, opaque IDs and modeled fields without editor controls", async () => {
|
|
renderPage();
|
|
await ready();
|
|
fireEvent.change(screen.getByLabelText(en.profileCvStructuredAwards), { target: { value: "Recorded award\nManual award" } });
|
|
fireEvent.change(screen.getByLabelText(en.profileCvLinkLabel), { target: { value: "Edited portfolio" } });
|
|
fireEvent.change(screen.getByLabelText(en.profileCvJobTitle), { target: { value: "Edited role" } });
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceImportTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(en.profileCvRawPanelTitle) }));
|
|
fireEvent.change(screen.getByLabelText(en.profileCvTextLabel), { target: { value: "Manually retained source\nNo extraction requested" } });
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceProfileTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1));
|
|
const saved = mockedApi.put.mock.calls[0][1] as { profile: StructuredCvProfile; cvText: string };
|
|
expect(saved).toEqual({ profile: {
|
|
...profile,
|
|
awards: ["Recorded award", "Manual award"],
|
|
contact: { ...profile.contact, links: [{ label: "Edited portfolio", url: "https://example.test/portfolio" }] },
|
|
jobs: [{ ...profile.jobs[0], title: "Edited role" }],
|
|
}, cvText: "Manually retained source\nNo extraction requested", expectedVersion: 7 });
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
expect(mockedApi.patch).not.toHaveBeenCalled();
|
|
expect(mockedApi.delete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test.each([409, 0])("a rejected save (%s) keeps the draft and its original revision for deliberate retry", async (status) => {
|
|
mockedApi.put.mockRejectedValueOnce({ response: { status, data: { code: "revision_conflict" } } });
|
|
renderPage();
|
|
await ready();
|
|
fireEvent.change(screen.getByLabelText(en.profileCvContactFullName), { target: { value: "Recoverable draft" } });
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
await editor().findByRole("alert");
|
|
expect(screen.getByLabelText(en.profileCvContactFullName)).toHaveValue("Recoverable draft");
|
|
expect(screen.getByText(en.assetsUnsavedChanges)).toBeInTheDocument();
|
|
expect(mockedApi.put).toHaveBeenLastCalledWith("/career/profile", expect.objectContaining({ expectedVersion: 7 }));
|
|
expect(readCount("/career/profile")).toBe(1);
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
|
expect(mockedApi.put.mock.calls[1][1]).toEqual(mockedApi.put.mock.calls[0][1]);
|
|
});
|
|
|
|
test("consecutive acknowledged career saves advance the revision returned with the saved snapshot", async () => {
|
|
mockedApi.put.mockResolvedValueOnce({ data: { ...sourceResponse().data, revision: 8 } } as any);
|
|
renderPage(); await ready();
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
await waitFor(() => expect(screen.getByRole("button", { name: en.profileSaveChanges })).toBeEnabled());
|
|
fireEvent.change(screen.getByLabelText(en.profileCvContactFullName), { target: { value: "Next snapshot" } });
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
|
expect(mockedApi.put.mock.calls.map(([, body]) => (body as any).expectedVersion)).toEqual([7, 8]);
|
|
});
|
|
|
|
test("a delayed prior lifecycle read cannot replace edits in the current career editor", async () => {
|
|
const oldRead = deferred<any>(); let reads = 0;
|
|
const get = mockedApi.get.getMockImplementation()!;
|
|
mockedApi.get.mockImplementation((url: string) => url === "/career/profile" && ++reads === 1 ? oldRead.promise : get(url));
|
|
render(<React.StrictMode><ToastProvider><I18nProvider><CareerProfilePage /></I18nProvider></ToastProvider></React.StrictMode>);
|
|
await ready();
|
|
fireEvent.change(screen.getByLabelText(en.profileCvContactFullName), { target: { value: "Current lifecycle draft" } });
|
|
await act(async () => oldRead.resolve({ data: { ...sourceResponse().data, revision: 2, profile: { ...profile, contact: { ...profile.contact, fullName: "Late old source" } } } }));
|
|
expect(screen.getByLabelText(en.profileCvContactFullName)).toHaveValue("Current lifecycle draft");
|
|
fireEvent.click(screen.getByRole("button", { name: en.profileSaveChanges }));
|
|
expect(mockedApi.put).toHaveBeenCalledWith("/career/profile", expect.objectContaining({ expectedVersion: 7 }));
|
|
});
|
|
|
|
test("a read-only provider cannot edit canonical fields or trigger canonical writes", async () => {
|
|
provider = "external";
|
|
renderPage();
|
|
await waitFor(() => expect(screen.getByLabelText(en.profileCvContactFullName)).toHaveValue("Recorded owner"));
|
|
fireEvent.click(screen.getByRole("button", { name: /Version history/ }));
|
|
expect(screen.getAllByRole("textbox").filter((field) => !(field as HTMLInputElement).disabled)).toEqual([]);
|
|
for (const name of [en.profileSaveChanges, en.careerRestoreVersion.replace("{version}", "3"), en.profileCvAddLink, en.profileCvStructuredAddLanguage, en.profileCvStructuredAddJob, en.profileCvStructuredAddEducation, en.profileCvStructuredAddOtherSection]) {
|
|
const control = screen.getByRole("button", { name });
|
|
expect(control).toBeDisabled();
|
|
control.click();
|
|
}
|
|
for (const button of screen.getAllByRole("button", { name: /^(Remove|Delete custom link|Bold|Italic|Underline|Link$)/ })) expect(button).toBeDisabled();
|
|
fireEvent.click(screen.getByRole("tab", { name: en.careerWorkspaceImportTab }));
|
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(en.profileCvRawPanelTitle) }));
|
|
expect(screen.getByLabelText(en.profileCvTextLabel)).toBeDisabled();
|
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
expect(mockedApi.patch).not.toHaveBeenCalled();
|
|
expect(mockedApi.delete).not.toHaveBeenCalled();
|
|
});
|