test(redesign): establish preservation baseline
Map 57 capability groups before restructuring. Add behavioral regression coverage and a standalone TypeScript gate; retain the native SQLite package failure as separate baseline debt.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { expect, test as base, type APIRequestContext } from "@playwright/test";
|
||||
|
||||
const apiUrl = "http://localhost:5302/api";
|
||||
const password = "E2ePassword123!";
|
||||
const canonical = {
|
||||
version: "1", contact: { fullName: "Åse Eksempel", headline: "Canonical engineer" },
|
||||
jobs: [{ title: "Engineer", company: "Nordlys AS", bullets: ["Built reliable APIs"] }],
|
||||
skills: ["C#", "Norsk"],
|
||||
};
|
||||
|
||||
async function login(api: APIRequestContext, email: string) {
|
||||
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
||||
const me = await (await api.get(`${apiUrl}/auth/me`)).json();
|
||||
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === "XSRF-TOKEN")!.value;
|
||||
return { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext as string };
|
||||
}
|
||||
|
||||
// Every case gets a real non-admin account in Playwright's disposable local API database.
|
||||
// No API responses are mocked; browser traffic cannot leave the local test environment.
|
||||
const test = base.extend<{ headers: Record<string, string> }>({
|
||||
headers: async ({ playwright, page, context }, provide) => {
|
||||
const admin = await playwright.request.newContext();
|
||||
try {
|
||||
const adminHeaders = await login(admin, "e2e@example.test");
|
||||
const email = `pres-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
|
||||
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
|
||||
const headers = await login(page.request, email);
|
||||
await context.route("**/*", route => {
|
||||
const url = new URL(route.request().url());
|
||||
return ["localhost", "127.0.0.1"].includes(url.hostname) || ["data:", "blob:", "about:"].includes(url.protocol)
|
||||
? route.continue() : route.abort();
|
||||
});
|
||||
await context.addInitScript(() => localStorage.setItem("uiLanguage", "en"));
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: canonical } })).status()).toBe(200);
|
||||
await provide(headers);
|
||||
} finally { await admin.dispose(); }
|
||||
},
|
||||
});
|
||||
|
||||
test("[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", async ({ page, headers }) => {
|
||||
const create = async (name: string) => {
|
||||
const response = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name } });
|
||||
expect(response.status()).toBe(200);
|
||||
return response.json();
|
||||
};
|
||||
const selected = await create("Selected CV");
|
||||
const sibling = await create("Sibling CV");
|
||||
const profileBefore = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(`/career/builder/${selected.id}`);
|
||||
await page.getByRole("tab", { name: "Content", exact: true }).click();
|
||||
await page.getByLabel("Headline override", { exact: true }).fill("Reviewed CV-only headline");
|
||||
await page.getByRole("button", { name: "Save now", exact: true }).click();
|
||||
await expect(page.getByText("Saved", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Preview", exact: true }).click();
|
||||
await expect(page.getByTitle("CV preview page 1")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await expect(page.getByLabel("Headline override", { exact: true })).toHaveValue("Reviewed CV-only headline");
|
||||
await page.reload();
|
||||
await page.getByRole("tab", { name: "Content", exact: true }).click();
|
||||
await expect(page.getByLabel("Headline override", { exact: true })).toHaveValue("Reviewed CV-only headline");
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await expect(page.getByTitle("CV preview page 1")).toBeVisible();
|
||||
expect((await (await page.request.get(`${apiUrl}/cv/variants/${selected.id}`)).json()).settings.headline).toBe("Reviewed CV-only headline");
|
||||
expect(await (await page.request.get(`${apiUrl}/cv/variants/${sibling.id}`)).json()).toEqual(sibling);
|
||||
expect((await (await page.request.get(`${apiUrl}/career/profile`)).json()).profile).toEqual(profileBefore.profile);
|
||||
});
|
||||
|
||||
test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", async ({ page, headers }) => {
|
||||
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Preservation AS" } });
|
||||
expect(companyResponse.status()).toBe(201);
|
||||
const company = await companyResponse.json();
|
||||
const jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: "Preserved workspace", status: "Saved" } });
|
||||
expect(jobResponse.ok()).toBe(true);
|
||||
const job = await jobResponse.json();
|
||||
const variantResponse = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Submitted CV", jobApplicationId: job.id } });
|
||||
expect(variantResponse.status()).toBe(200);
|
||||
const variant = await variantResponse.json();
|
||||
const jobUrl = `${apiUrl}/jobapplications/${job.id}`;
|
||||
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Original reviewed letter", source: "manual" } })).status()).toBe(200);
|
||||
const bytes = Buffer.from("Original attachment — æøå\n", "utf8");
|
||||
const upload = await page.request.post(`${apiUrl}/attachments`, { headers, multipart: { jobId: String(job.id), files: { name: "evidence.txt", mimeType: "text/plain", buffer: bytes } } });
|
||||
expect(upload.ok()).toBe(true);
|
||||
const [attachment] = await (await page.request.get(`${apiUrl}/attachments/${job.id}`)).json();
|
||||
await page.goto(`/applications/${job.id}?section=cv`);
|
||||
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cv$`));
|
||||
const captured = page.waitForResponse(response => response.url() === `${jobUrl}/submitted-packages` && response.request().method() === 'POST');
|
||||
await page.getByRole("button", { name: "Freeze current package", exact: true }).click();
|
||||
const captureResponse = await captured;
|
||||
// P2-D01: native SQLite rejects the service's DateTimeOffset ORDER BY. Only
|
||||
// this known response is expected; setup failures and later archive failures
|
||||
// after a successful capture must remain ordinary failing assertions.
|
||||
if (captureResponse.status() === 500) {
|
||||
test.fail(true, 'P2-D01: SQLite submitted-package capture; phase-2/BASELINE_DEFECTS.md');
|
||||
}
|
||||
expect(captureResponse.status()).toBe(200);
|
||||
await expect(page.getByText("Submission package v1", { exact: true })).toBeVisible();
|
||||
const [snapshot] = await (await page.request.get(`${jobUrl}/submitted-packages`)).json();
|
||||
const before = await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json();
|
||||
expect(before.coverLetterText).toBe("Original reviewed letter");
|
||||
expect(before.renderedCvHtml).toContain("Canonical engineer");
|
||||
expect(snapshot.cvVariantId).toBe(variant.id);
|
||||
expect(snapshot.attachments).toHaveLength(1);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Later letter", source: "manual" } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/cv/variants/${variant.id}`, { headers, data: { name: "Later CV", settings: { ...variant.settings, headline: "Later headline" } } })).status()).toBe(200);
|
||||
expect((await page.request.delete(`${apiUrl}/attachments/${attachment.id}`, { headers })).status()).toBe(204);
|
||||
expect(await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json()).toEqual(before);
|
||||
const archived = await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}/attachments/${snapshot.attachments[0].id}`);
|
||||
expect(archived.status()).toBe(200);
|
||||
expect(await archived.body()).toEqual(bytes);
|
||||
await page.reload();
|
||||
await page.getByRole("button", { name: "View preserved package", exact: true }).click();
|
||||
await expect(page.getByRole("dialog").getByText("Original reviewed letter", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", async ({ page, headers }) => {
|
||||
expect((await page.request.get(`${apiUrl}/auth/me`, { headers })).status()).toBe(200);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/settings");
|
||||
await page.getByLabel("Settings section", { exact: true }).click();
|
||||
await page.getByRole("option", { name: "Follow-ups", exact: true }).click();
|
||||
const field = page.getByLabel("Applied: follow-up days", { exact: true });
|
||||
await field.fill("5");
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await expect(field).toHaveValue("5");
|
||||
const saveRules = page.waitForResponse(response => response.url() === `${apiUrl}/rules` && response.request().method() === 'PUT');
|
||||
await page.getByRole("button", { name: "Save Rules", exact: true }).click();
|
||||
expect((await saveRules).status()).toBe(204);
|
||||
await expect.poll(async () => (await (await page.request.get(`${apiUrl}/rules`)).json()).appliedFollowUpDays).toBe(5);
|
||||
await page.getByRole("tab", { name: "Notifications", exact: true }).click();
|
||||
await page.getByLabel("Email reminders for follow-ups", { exact: true }).uncheck();
|
||||
await page.getByRole("button", { name: "Save notification settings", exact: true }).click();
|
||||
await expect.poll(async () => (await (await page.request.get(`${apiUrl}/notification-settings`)).json()).emailFollowUpRemindersEnabled).toBe(false);
|
||||
await page.reload();
|
||||
await page.getByRole("tab", { name: "Notifications", exact: true }).click();
|
||||
await expect(page.getByLabel("Email reminders for follow-ups", { exact: true })).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("[PRES-S03] rule changes persist through the real API", async ({ page, headers }) => {
|
||||
const settings = await (await page.request.get(`${apiUrl}/rules`)).json();
|
||||
const saved = await page.request.put(`${apiUrl}/rules`, { headers, data: { ...settings, appliedFollowUpDays: 5 } });
|
||||
expect(saved.status()).toBe(204);
|
||||
expect((await (await page.request.get(`${apiUrl}/rules`)).json()).appliedFollowUpDays).toBe(5);
|
||||
});
|
||||
|
||||
test("[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy workspace entry preserve the selected job", async ({ page, headers }) => {
|
||||
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Command AS" } });
|
||||
expect(companyResponse.status()).toBe(201);
|
||||
const jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers,
|
||||
data: { companyId: (await companyResponse.json()).id, jobTitle: "Command engineer", status: "Saved" } });
|
||||
expect(jobResponse.ok()).toBe(true);
|
||||
const job = await jobResponse.json();
|
||||
await page.goto("/dashboard");
|
||||
await expect(page.getByRole("heading", { name: "Dashboard", exact: true })).toBeVisible();
|
||||
await page.keyboard.press("Control+k");
|
||||
await page.getByRole("dialog").getByRole("textbox").fill("Command engineer");
|
||||
await page.getByText("Command AS - Command engineer", { exact: true }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`));
|
||||
await expect(page.getByRole("heading", { name: "Command engineer", exact: true })).toBeVisible();
|
||||
await page.goto(`/applications/${job.id}?section=match`);
|
||||
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=match$`));
|
||||
await expect(page.getByRole("tab", { name: "Analysis", exact: true })).toHaveAttribute("aria-selected", "true");
|
||||
await page.keyboard.press("Control+k");
|
||||
await page.getByRole("dialog").getByText("Add new job", { exact: true }).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
expect((await (await page.request.get(`${apiUrl}/jobapplications`)).json()).total).toBe(1);
|
||||
});
|
||||
|
||||
test("[PRES-A01 PRES-S04 PRES-E07] non-admin navigation cannot grant API privileges or unlock CV AI", async ({ page, headers }) => {
|
||||
await page.goto("/dashboard");
|
||||
await expect(page.getByRole("heading", { name: "Dashboard", exact: true })).toBeVisible();
|
||||
for (const name of ["Users", "Audit log", "System"]) await expect(page.getByRole("button", { name, exact: true })).toHaveCount(0);
|
||||
const denied = page.waitForResponse(response => response.url() === `${apiUrl}/users`);
|
||||
await page.goto("/admin/users");
|
||||
expect((await denied).status()).toBe(403);
|
||||
await expect(page.getByText("e2e@example.test", { exact: true })).toHaveCount(0);
|
||||
expect((await page.request.post(`${apiUrl}/users`, { headers, data: { email: "must-not-exist@example.test", password, roles: ["Admin"] } })).status()).toBe(403);
|
||||
const created = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Free CV" } });
|
||||
expect(created.status()).toBe(200);
|
||||
await page.goto(`/career/builder/${(await created.json()).id}`);
|
||||
await page.getByRole("tab", { name: "AI Tools", exact: true }).click();
|
||||
const actions = page.getByRole("button", { name: "Review suggestion", exact: true });
|
||||
await expect(actions).toHaveCount(3);
|
||||
for (const action of await actions.all()) await expect(action).toBeDisabled();
|
||||
expect((await page.request.post(`${apiUrl}/cv/ai/assist`, { headers, data: { action: "improve", text: "Safe test text" } })).status()).toBe(403);
|
||||
expect((await (await page.request.get(`${apiUrl}/auth/me`)).json()).roles).toEqual([]);
|
||||
});
|
||||
@@ -35,6 +35,7 @@
|
||||
"build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit --incremental false",
|
||||
"test": "jest",
|
||||
"test:pdf-admission": "playwright test --config=playwright.pdf.config.ts",
|
||||
"test:rate-limits": "playwright test --config=playwright.rate-limits.config.ts",
|
||||
|
||||
@@ -69,3 +69,44 @@ test("section AI controls render in Bokmål without changing the document langua
|
||||
expect(screen.getByLabelText("Språk for resultat")).toHaveTextContent("Norsk bokmål");
|
||||
expect(screen.getByRole("button", { name: "Foreslå" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each(["Reject", "Close"])("[PRES-C11] %s leaves the source unchanged and removes the suggestion", async (action) => {
|
||||
mockedApi.post.mockResolvedValue({ data: { result: "Unaccepted draft" } } as never);
|
||||
const onApply = renderAssistant();
|
||||
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
|
||||
expect(await screen.findByDisplayValue("Unaccepted draft")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: action }));
|
||||
expect(screen.queryByDisplayValue("Unaccepted draft")).not.toBeInTheDocument();
|
||||
expect(onApply).not.toHaveBeenCalled();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
if (action === "Close") fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
|
||||
expect(screen.queryByRole("button", { name: "Apply" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("[PRES-C11] applies the user's edited suggestion only after acceptance", async () => {
|
||||
mockedApi.post.mockResolvedValue({ data: { result: "Proposed wording" } } as never);
|
||||
const onApply = renderAssistant();
|
||||
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
|
||||
fireEvent.change(await screen.findByDisplayValue("Proposed wording"), { target: { value: "My reviewed wording" } });
|
||||
expect(onApply).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
|
||||
expect(onApply).toHaveBeenCalledTimes(1);
|
||||
expect(onApply).toHaveBeenCalledWith("My reviewed wording");
|
||||
});
|
||||
|
||||
test("[PRES-C11] a failed suggestion can be retried without applying or saving content", async () => {
|
||||
mockedApi.post.mockRejectedValueOnce(new Error("offline"))
|
||||
.mockResolvedValueOnce({ data: { result: "Recovered suggestion" } } as never);
|
||||
const onApply = renderAssistant();
|
||||
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
|
||||
expect(await screen.findByRole("alert")).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Suggest" })).toBeEnabled());
|
||||
expect(onApply).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
|
||||
expect(await screen.findByDisplayValue("Recovered suggestion")).toBeInTheDocument();
|
||||
expect(onApply).not.toHaveBeenCalled();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -261,6 +261,23 @@ test("AI cover letter suggestions use the linked CV and require explicit apply",
|
||||
));
|
||||
});
|
||||
|
||||
test("[PRES-W06] rejecting an AI cover letter preserves the saved draft without a write", async () => {
|
||||
routeGet();
|
||||
mockedApi.post.mockResolvedValue({ data: { result: { text: "Unaccepted letter" } } } as never);
|
||||
const onDirtyChange = jest.fn();
|
||||
render(<ApplicationCoverLetterSection jobId={7} onDirtyChange={onDirtyChange} />);
|
||||
await screen.findByText(/application's full job advert and analysis/i);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Generate" }));
|
||||
await screen.findByText("Unaccepted letter");
|
||||
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
|
||||
expect(screen.queryByText("Unaccepted letter")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
expect(onDirtyChange).not.toHaveBeenCalledWith(true);
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("a failed load surfaces an error", async () => {
|
||||
mockedApi.get.mockRejectedValue(new Error("boom"));
|
||||
|
||||
|
||||
@@ -345,3 +345,52 @@ test('preview failure is visible and retryable without leaving the editor', asyn
|
||||
expect(screen.getByTitle('CV preview')).toHaveAttribute('sandbox', 'allow-same-origin allow-scripts');
|
||||
await waitFor(() => expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
async function reviewDocumentSuggestion(result: string) {
|
||||
routeGet(() => Promise.resolve({ data: variant }), { headline: 'Canonical headline', sections: [] });
|
||||
mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url === '/cv/ai/assist'
|
||||
? { result } : { html: '<p>Preview</p>' } } as never));
|
||||
mockedApi.put.mockImplementation((_url, body) => Promise.resolve({ data: { ...variant, ...(body as object) } } as never));
|
||||
renderAt(3);
|
||||
await screen.findByDisplayValue('Backend CV');
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' }));
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]);
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.objectContaining({
|
||||
action: 'document-translate', text: JSON.stringify([{ id: 'headline', items: ['Canonical headline'] }]),
|
||||
})));
|
||||
}
|
||||
|
||||
test.each(['Apply', 'Reject'])('[PRES-C11 PRES-C07] whole-document AI %s changes only an explicitly accepted CV override', async action => {
|
||||
await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }]));
|
||||
expect(await screen.findByText('Review 1 proposed change(s)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Canonical headline')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reviewed headline')).toBeInTheDocument();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: action }));
|
||||
expect(screen.queryByText('Review 1 proposed change(s)')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Content' }));
|
||||
expect(await screen.findByLabelText('Headline override')).toHaveValue(action === 'Apply' ? 'Reviewed headline' : '');
|
||||
if (action === 'Apply') {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save now' }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({
|
||||
settings: expect.objectContaining({ headline: 'Reviewed headline' }),
|
||||
})));
|
||||
expect(mockedApi.put.mock.calls.every(([url]) => url === '/cv/variants/3')).toBe(true);
|
||||
} else {
|
||||
expect(screen.getByText('Saved')).toBeInTheDocument();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
['invalid JSON', 'not JSON'],
|
||||
['missing original ID', JSON.stringify([{ id: 'unknown', items: ['Wrong document'] }])],
|
||||
['duplicate original ID', JSON.stringify([{ id: 'headline', items: ['First'] }, { id: 'headline', items: ['Second'] }])],
|
||||
])('[PRES-C11] unsafe whole-document response (%s) cannot be applied', async (_case, result) => {
|
||||
await reviewDocumentSuggestion(result);
|
||||
expect(await screen.findByText('AI suggestion failed.')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Content' }));
|
||||
expect(await screen.findByLabelText('Headline override')).toHaveValue('');
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { 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>);
|
||||
fireEvent.change(screen.getByRole("textbox"), { 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();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import SettingsView from './components/SettingsView';
|
||||
@@ -136,3 +136,30 @@ test('AI privacy settings are server-backed and external processing is local-onl
|
||||
externalProcessingAllowed: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('[PRES-S03] failed notification save preserves the draft for an explicit retry', async () => {
|
||||
mockedApi.put.mockRejectedValueOnce(new Error('offline'));
|
||||
renderView();
|
||||
fireEvent.click(screen.getByRole('tab', { name: /notifications/i }));
|
||||
const preference = await screen.findByLabelText(/email reminders for follow-ups/i);
|
||||
await waitFor(() => expect(preference).toBeChecked());
|
||||
fireEvent.click(preference);
|
||||
fireEvent.click(screen.getByRole('button', { name: /save notification settings/i }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(/could not|failed/i);
|
||||
expect(preference).not.toBeChecked();
|
||||
fireEvent.click(screen.getByRole('button', { name: /save notification settings/i }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
||||
expect(mockedApi.put).toHaveBeenLastCalledWith('/notification-settings', { emailFollowUpRemindersEnabled: false });
|
||||
await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('[PRES-S03 PRES-W11] saving one follow-up rule preserves the other scenarios', async () => {
|
||||
renderView();
|
||||
fireEvent.click(screen.getByRole('tab', { name: /follow-ups/i }));
|
||||
fireEvent.change(await screen.findByLabelText('Applied: follow-up days'), { target: { value: '5' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/rules', {
|
||||
id: 1, appliedFollowUpDays: 5, appliedGhostDays: 30,
|
||||
offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14,
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user