Files
jobtrackingapp/job-tracker-ui/src/calendar-agenda.test.tsx
T

91 lines
5.9 KiB
TypeScript

import React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { api } from './api';
import CalendarAgenda from './components/CalendarAgenda';
import { I18nProvider } from './i18n/I18nProvider';
import { translations } from './i18n/translations';
import { setAuthUserKey } from './auth';
jest.mock('./api', () => ({ api: { get: jest.fn() }, getApiErrorMessage: (_: unknown, fallback: string) => fallback }));
const get = api.get as jest.Mock;
const now = new Date();
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const row = { jobApplicationId: 42, jobTitle: 'Recorded teacher role', companyName: 'School', kind: 'deadline', date: `${month}-15`, recordedAt: `${month}-15T23:30:00` };
const mount = () => render(<I18nProvider><MemoryRouter><CalendarAgenda /></MemoryRouter></I18nProvider>);
beforeEach(() => { localStorage.clear(); document.documentElement.lang = 'en'; get.mockReset(); get.mockResolvedValue({ data: [row] }); });
afterEach(() => jest.restoreAllMocks());
test('agenda prioritises upcoming recorded dates while retaining earlier dates in the month', async () => {
jest.spyOn(Date, 'now').mockReturnValue(new Date(now.getFullYear(), now.getMonth(), 16, 12).getTime());
get.mockResolvedValue({ data: [row, { ...row, jobApplicationId: 43, jobTitle: 'Upcoming nursing role', date: `${month}-20`, recordedAt: `${month}-20T00:00:00` }] });
mount(); await screen.findByRole('link', { name: /Upcoming nursing role/ });
expect(screen.getAllByRole('link').map(link => link.getAttribute('href'))).toEqual(['/jobs/43', '/jobs/42']);
});
test('agenda defaults to recorded dates and links exact application without a fabricated appointment time', async () => {
mount();
const link = await screen.findByRole('link', { name: /Recorded teacher role/ });
expect(link).toHaveAttribute('href', '/jobs/42');
expect(screen.getByText(translations.en.calendarDateScope)).toBeVisible();
expect(document.querySelector('time')).toHaveAttribute('datetime', row.recordedAt);
expect(screen.queryByText('23:30')).not.toBeInTheDocument();
expect(get).toHaveBeenCalledWith('/calendar/agenda', { params: { from: `${month}-01`, to: `${month}-${new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()}` } });
});
test('month toggle persists only view preference and selecting a day keeps linked events available', async () => {
const first = mount(); await screen.findByRole('link', { name: /Recorded teacher role/ });
fireEvent.click(screen.getByRole('button', { name: translations.en.calendarMonth }));
const table = screen.getByRole('table', { name: translations.en.calendarMonth });
fireEvent.click(within(table).getByRole('button', { name: /15.*1/ }));
expect(screen.getByRole('link', { name: /Recorded teacher role/ })).toBeVisible();
expect(get).toHaveBeenCalledTimes(1);
first.unmount(); mount();
expect(await screen.findByRole('table', { name: translations.en.calendarMonth })).toBeVisible();
});
test('locale changes translate controls without repeating the read or translating source text', async () => {
mount(); await screen.findByRole('link', { name: /Recorded teacher role/ });
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
expect(screen.getByRole('button', { name: translations.nb.calendarMonth })).toBeVisible();
expect(screen.getByRole('combobox', { name: translations.nb.calendarSelectedMonth })).toHaveTextContent(new Intl.DateTimeFormat('nb-NO', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${month}-01T12:00:00Z`)));
expect(screen.getByRole('link', { name: /Recorded teacher role/ })).toBeVisible();
expect(get).toHaveBeenCalledTimes(1);
});
test('failed read offers bounded retry and never claims an empty successful calendar', async () => {
get.mockRejectedValueOnce(new Error('offline'));
mount(); await screen.findByRole('alert');
expect(screen.queryByText(translations.en.calendarAgendaEmpty)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: translations.en.retry }));
expect(await screen.findByRole('link', { name: /Recorded teacher role/ })).toBeVisible();
expect(get).toHaveBeenCalledTimes(2);
});
test('invalid and unknown-kind fixture data is not turned into guessed events', async () => {
get.mockResolvedValue({ data: [{ ...row, date: `${month}-99` }, { ...row, kind: 'interview' }] });
mount(); expect(await screen.findByRole('alert')).toBeVisible();
expect(screen.queryByText(translations.en.calendarAgendaEmpty)).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: /Recorded teacher role/ })).not.toBeInTheDocument();
});
test('month navigation requests only the selected calendar range', async () => {
mount(); await screen.findByRole('link', { name: /Recorded teacher role/ });
get.mockResolvedValue({ data: [] });
fireEvent.click(screen.getByRole('button', { name: translations.en.calendarNextMonth }));
const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
const prefix = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}`;
expect(get).toHaveBeenLastCalledWith('/calendar/agenda', { params: { from: `${prefix}-01`, to: `${prefix}-${new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate()}` } });
expect(await screen.findByText(translations.en.calendarAgendaEmpty)).toBeVisible();
});
test('view preference is scoped to the signed-in owner', async () => {
setAuthUserKey('calendar-owner-one', false);
const first = mount(); await screen.findByRole('link', { name: /Recorded teacher role/ });
fireEvent.click(screen.getByRole('button', { name: translations.en.calendarMonth })); first.unmount();
setAuthUserKey('calendar-owner-two', false); mount();
await screen.findByRole('link', { name: /Recorded teacher role/ });
expect(screen.queryByRole('table', { name: translations.en.calendarMonth })).not.toBeInTheDocument();
});