fix(calendar): refresh recorded dates after follow-up changes
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import CalendarPage from './views/CalendarPage';
|
||||
import { I18nProvider } from './i18n/I18nProvider';
|
||||
import { ToastProvider } from './toast';
|
||||
import { api } from './api';
|
||||
|
||||
jest.mock('./api', () => ({ api: { get: jest.fn(), patch: jest.fn() }, getApiErrorMessage: (_error: unknown, fallback: string) => fallback }));
|
||||
const mocked = api as jest.Mocked<typeof api>;
|
||||
const calendarReads = () => mocked.get.mock.calls.filter(([url]) => url === '/calendar/agenda').length;
|
||||
|
||||
beforeEach(() => { localStorage.clear(); jest.clearAllMocks(); jest.useFakeTimers({ now: new Date('2026-11-04T12:00:00Z') }); });
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
test('successful scheduling and clearing refresh the adjacent calendar without resetting its view; failed writes do not refresh it', async () => {
|
||||
let followUpAt: string | null = null;
|
||||
const job = () => ({ id: 42, jobTitle: 'Synthetic nurse', company: { id: 1, name: 'Synthetic employer' }, status: 'Waiting', needsFollowUp: true, followUpAt });
|
||||
mocked.get.mockImplementation(url => Promise.resolve({ data: url === '/jobapplications/reminders' ? [job()] : url === '/calendar/agenda' && followUpAt ? [{ jobApplicationId: 42, jobTitle: job().jobTitle, companyName: job().company.name, kind: 'follow-up', date: followUpAt, recordedAt: followUpAt }] : [] } as never));
|
||||
mocked.patch.mockImplementation((_url, body) => { followUpAt = (body as { followUpAt: string | null }).followUpAt; return Promise.resolve({ data: {} } as never); });
|
||||
render(<I18nProvider><ToastProvider><MemoryRouter><CalendarPage /></MemoryRouter></ToastProvider></I18nProvider>);
|
||||
const reminder = await screen.findByRole('article', { name: 'Synthetic nurse' });
|
||||
expect(calendarReads()).toBe(1);
|
||||
fireEvent.click(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' }));
|
||||
expect(await screen.findByRole('link', { name: /Synthetic nurse/ })).toBeVisible();
|
||||
expect(mocked.patch).toHaveBeenCalledWith('/jobapplications/42/followup', { followUpAt: '2026-11-07' });
|
||||
expect(calendarReads()).toBe(2);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Month' }));
|
||||
expect(screen.getByRole('table', { name: 'Month' })).toBeVisible();
|
||||
fireEvent.click(within(reminder).getByRole('button', { name: 'Clear follow-up: Synthetic nurse' }));
|
||||
await waitFor(() => expect(calendarReads()).toBe(3));
|
||||
await waitFor(() => expect(screen.getByRole('table', { name: 'Month' })).toBeVisible());
|
||||
expect(screen.getByRole('button', { name: 'Month' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByRole('combobox', { name: /^Selected month/ })).toHaveTextContent('November 2026');
|
||||
mocked.patch.mockRejectedValueOnce({ response: { status: 503 } });
|
||||
fireEvent.click(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' }));
|
||||
await waitFor(() => expect(mocked.patch).toHaveBeenCalledTimes(3));
|
||||
await waitFor(() => expect(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' })).toBeEnabled());
|
||||
expect(calendarReads()).toBe(3);
|
||||
});
|
||||
@@ -16,7 +16,7 @@ function calendarDate(value: string) {
|
||||
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value ? date : null;
|
||||
}
|
||||
|
||||
export default function CalendarAgenda() {
|
||||
export default function CalendarAgenda({ refreshToken = 0 }: { refreshToken?: number }) {
|
||||
const { language, t } = useI18n();
|
||||
const preferenceKey = getUserScopedStorageKey('jobtracker.calendarView');
|
||||
const [view, setView] = useState<'agenda' | 'month'>(() => { try { return localStorage.getItem(preferenceKey) === 'month' ? 'month' : 'agenda'; } catch { return 'agenda'; } });
|
||||
@@ -32,7 +32,7 @@ export default function CalendarAgenda() {
|
||||
if (!Array.isArray(response.data) || response.data.some(item => !Number.isSafeInteger(item.jobApplicationId) || item.jobApplicationId <= 0 || typeof item.jobTitle !== 'string' || !['deadline', 'follow-up'].includes(item.kind) || typeof item.recordedAt !== 'string' || !calendarDate(item.date) || item.date < from || item.date > to)) throw new Error('Invalid calendar response');
|
||||
return response.data;
|
||||
}, [from, to]);
|
||||
const resource = useViewResource(load, { initialData: [] as CalendarItem[], errorMessage: '', deps: [from, to] });
|
||||
const resource = useViewResource(load, { initialData: [] as CalendarItem[], errorMessage: '', deps: [from, to, refreshToken] });
|
||||
const locale = language === 'nb' ? 'nb-NO' : 'en-GB';
|
||||
const formatDate = (date: string) => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone: 'UTC' }).format(calendarDate(date)!);
|
||||
const byDay = useMemo(() => {
|
||||
|
||||
@@ -106,7 +106,7 @@ function ReminderSection({ title, items, disabled, onOpen, onSetFollowUp }: { ti
|
||||
);
|
||||
}
|
||||
|
||||
export default function RemindersView() {
|
||||
export default function RemindersView({ onDatesChanged }: { onDatesChanged?: () => void }) {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
@@ -145,6 +145,7 @@ export default function RemindersView() {
|
||||
setFailure(null);
|
||||
try {
|
||||
await api.patch(`/jobapplications/${job.id}/followup`, { followUpAt: date });
|
||||
onDatesChanged?.();
|
||||
toast(daysFromNow === null ? t("remindersFollowUpCleared") : t("remindersFollowUpSet"), "success");
|
||||
await remindersResource.reload();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Stack } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import CalendarAgenda from '../components/CalendarAgenda';
|
||||
import RemindersView from '../components/RemindersView';
|
||||
|
||||
export default function CalendarPage() {
|
||||
return <Stack spacing={3} sx={{ minWidth: 0 }}><CalendarAgenda /><RemindersView /></Stack>;
|
||||
const [calendarRefresh, setCalendarRefresh] = useState(0);
|
||||
return <Stack spacing={3} sx={{ minWidth: 0 }}><CalendarAgenda refreshToken={calendarRefresh} /><RemindersView onDatesChanged={() => setCalendarRefresh(value => value + 1)} /></Stack>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user