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:
cesnimda
2026-09-10 12:53:06 +02:00
parent 2d0f131a99
commit d6d3dd977d
14 changed files with 835 additions and 3 deletions
@@ -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();
});
+28 -1
View File
@@ -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,
}));
});