fix(settings): preserve Rules drafts across internal tabs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import SettingsView from './components/SettingsView';
|
||||
@@ -181,3 +181,122 @@ test('[PRES-S03 PRES-J05] table and theme preferences keep their independent cal
|
||||
expect(onThemeModeChange).toHaveBeenCalledWith('light');
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
async function openRules() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: /follow-ups/i }));
|
||||
const field = await screen.findByLabelText('Applied: follow-up days');
|
||||
await waitFor(() => expect(field).toHaveValue(14));
|
||||
return field;
|
||||
}
|
||||
|
||||
function rulesRoundTrip() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: /notifications/i }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /follow-ups/i }));
|
||||
}
|
||||
|
||||
test('[PRES-S03 W11] the Settings session retains independent pending Rules fields across tab unmounts', async () => {
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
fireEvent.change(screen.getByLabelText('Offer: follow-up days'), { target: { value: '9' } });
|
||||
rulesRoundTrip();
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(5);
|
||||
expect(screen.getByLabelText('Offer: follow-up days')).toHaveValue(9);
|
||||
expect(screen.getByLabelText('Feedback: follow-up days')).toHaveValue(7);
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/unsaved/i);
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('[PRES-S03] failed Rules save remains actionable after tab return and explicit retry clears dirty state', async () => {
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
mockedApi.put.mockRejectedValueOnce(new Error('offline'));
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(/failed to save rules/i);
|
||||
rulesRoundTrip();
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(5);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(/failed to save rules/i);
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/unsaved/i);
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument());
|
||||
expect(screen.queryByText('Failed to save rules.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('[PRES-S03] invalid and empty Rules input and associated validation survive internal navigation', async () => {
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '' } });
|
||||
fireEvent.change(screen.getByLabelText('Offer: follow-up days'), { target: { value: '366' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
rulesRoundTrip();
|
||||
const empty = screen.getByLabelText('Applied: follow-up days');
|
||||
expect(empty).toHaveValue(null);
|
||||
expect(empty).toHaveAttribute('aria-invalid', 'true');
|
||||
expect(empty).toHaveAccessibleDescription(/whole number.*1.*365/i);
|
||||
expect(screen.getByLabelText('Offer: follow-up days')).toHaveValue(366);
|
||||
fireEvent.change(empty, { target: { value: '5' } });
|
||||
fireEvent.change(screen.getByLabelText('Offer: follow-up days'), { target: { value: '9' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test('[PRES-S03] explicit discard resets only the Rules draft to its saved baseline without writing', async () => {
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
rulesRoundTrip();
|
||||
fireEvent.click(screen.getByRole('button', { name: /discard changes/i }));
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(14);
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('[PRES-S03] later edits stay dirty when an earlier Rules save completes after a tab switch', async () => {
|
||||
let finish!: (value: any) => void;
|
||||
mockedApi.put.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; }));
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
fireEvent.change(screen.getByLabelText('Applied: follow-up days'), { target: { value: '6' } });
|
||||
rulesRoundTrip();
|
||||
await act(async () => finish({ status: 204 }));
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(6);
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/unsaved/i);
|
||||
fireEvent.click(screen.getByRole('button', { name: /discard changes/i }));
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(5);
|
||||
});
|
||||
|
||||
test('[PRES-S03 E09] leaving the Settings session disposes its draft and pending responses', async () => {
|
||||
let finish!: (value: any) => void;
|
||||
mockedApi.put.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; }));
|
||||
const first = renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
|
||||
first.unmount();
|
||||
renderView();
|
||||
await openRules();
|
||||
await act(async () => finish({ status: 204 }));
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(14);
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('[PRES-S03] switching locale while Rules is hidden does not reload or overwrite its pending draft', async () => {
|
||||
renderView();
|
||||
fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
const reads = mockedApi.get.mock.calls.filter(([url]) => url === '/rules').length;
|
||||
fireEvent.click(screen.getByRole('tab', { name: /general/i }));
|
||||
fireEvent.mouseDown(screen.getByRole('combobox', { name: 'Preferred language' }));
|
||||
fireEvent.click(screen.getByRole('option', { name: 'Norwegian Bokmål' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Oppfølging' }));
|
||||
expect(screen.getByLabelText('Søkt: oppfølgingsdager')).toHaveValue(5);
|
||||
expect(mockedApi.get.mock.calls.filter(([url]) => url === '/rules')).toHaveLength(reads);
|
||||
});
|
||||
|
||||
test('[PRES-S03] Rules draft updates do not refetch an unrelated notification preference', async () => {
|
||||
renderView();
|
||||
const field = await openRules();
|
||||
const reads = mockedApi.get.mock.calls.filter(([url]) => url === '/notification-settings').length;
|
||||
fireEvent.change(field, { target: { value: '5' } });
|
||||
fireEvent.change(field, { target: { value: '6' } });
|
||||
await act(async () => undefined);
|
||||
expect(mockedApi.get.mock.calls.filter(([url]) => url === '/notification-settings')).toHaveLength(reads);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user