Files
jobtrackingapp/job-tracker-ui/src/dashboard-customisation.test.tsx
T

96 lines
6.6 KiB
TypeScript

import React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import { I18nProvider } from './i18n/I18nProvider';
import { ConfirmProvider } from './confirm';
import { api } from './api';
const mocked = api as jest.Mocked<typeof api>;
let stored: any;
const stats = { total: 2, active: 2, byStatus: { Applied: 2 }, appliedLast30Days: 2 };
function response(url: string) {
if (url.endsWith('/preferences/dashboard')) return stored;
if (url.endsWith('/stats')) return stats;
if (url.endsWith('/analytics-overview')) return { funnel: [{ label: 'Applied', count: 2 }], totalActive: 2, totalResponses: 0, responseRateBySource: [], topCompanies: [], timeInStage: [], salaryInsights: [] };
if (url.endsWith('/tag-trends')) return { months: [], series: [] };
return [];
}
function open() {
const router = createMemoryRouter([{ path: '/dashboard', element: <DashboardView /> }, { path: '/jobs', element: <div>Jobs destination</div> }], { initialEntries: ['/dashboard'] });
return render(<I18nProvider><ConfirmProvider><RouterProvider router={router} /></ConfirmProvider></I18nProvider>);
}
beforeEach(() => {
localStorage.clear(); stored = { version: 1, revision: 0, widgets: [] };
mocked.get.mockImplementation((url: string) => Promise.resolve({ data: response(url) }) as any);
mocked.put.mockReset(); mocked.put.mockImplementation((_url: string, body: any) => { stored = { ...body, revision: body.revision + 1 }; return Promise.resolve({ data: stored }) as any; });
});
async function edit() { fireEvent.click(await screen.findByRole('button', { name: 'Customize dashboard' })); return screen.findByRole('region', { name: 'Editing dashboard' }); }
test('visibility is a preview; cancel restores the saved layout without a write', async () => {
open(); await screen.findByText('Tracked jobs');
const editor = await edit(); fireEvent.click(within(editor).getByRole('checkbox', { name: 'Summary cards' }));
expect(screen.getByRole('region', { name: 'Dashboard preview' })).toBeInTheDocument();
expect(screen.queryByText('Tracked jobs')).not.toBeInTheDocument();
fireEvent.click(within(editor).getByRole('button', { name: 'Cancel' }));
const dialog = await screen.findByRole('dialog'); fireEvent.click(within(dialog).getByRole('button', { name: 'Discard changes' }));
await screen.findByText('Tracked jobs'); expect(mocked.put).not.toHaveBeenCalled();
await waitFor(() => expect(screen.getByRole('button', { name: 'Customize dashboard' })).toHaveFocus());
});
test('failed save keeps the draft and retry persists visibility, order and size', async () => {
open(); const editor = await edit();
fireEvent.click(within(editor).getByRole('checkbox', { name: 'Summary cards' }));
fireEvent.click(within(editor).getByRole('button', { name: 'Move Today earlier' }));
fireEvent.change(within(editor).getByLabelText('Size for Today'), { target: { value: 'full' } });
mocked.put.mockRejectedValueOnce(new Error('offline'));
fireEvent.click(within(editor).getByRole('button', { name: 'Save layout' }));
await screen.findByText('Your layout could not be saved. Your changes are still here. Try again.');
expect(within(editor).getByRole('checkbox', { name: 'Summary cards' })).not.toBeChecked();
fireEvent.click(within(editor).getByRole('button', { name: 'Save layout' }));
await waitFor(() => expect(screen.queryByRole('region', { name: 'Editing dashboard' })).not.toBeInTheDocument());
expect(stored.widgets[0]).toEqual({ id: 'attention', visible: true, size: 'full' });
expect(stored.widgets.find((widget: any) => widget.id === 'cards').visible).toBe(false);
expect(screen.queryByText('Tracked jobs')).not.toBeInTheDocument();
});
test('all hidden widgets leave an accessible recovery and reset remains cancellable', async () => {
open(); const editor = await edit();
for (const checkbox of within(editor).getAllByRole('checkbox')) fireEvent.click(checkbox);
await screen.findByText('Your dashboard sections are hidden.');
fireEvent.click(within(editor).getByRole('button', { name: 'Reset to default' }));
const dialog = await screen.findByRole('dialog'); fireEvent.click(within(dialog).getByRole('button', { name: 'Reset to default' }));
await waitFor(() => expect(within(editor).getAllByRole('checkbox').every(item => (item as HTMLInputElement).checked)).toBe(true));
expect(mocked.put).not.toHaveBeenCalled();
});
test('keyboard ordering keeps focus and announces the resulting position without refetching reports', async () => {
open(); const editor = await edit(); const before = mocked.get.mock.calls.length;
const move = within(editor).getByRole('button', { name: 'Move Today earlier' }); move.focus(); fireEvent.click(move);
await waitFor(() => expect(within(editor).getByRole('button', { name: 'Move Today later' })).toHaveFocus());
expect(screen.getByText('Today moved to position 1 of 9.')).toBeInTheDocument();
expect(mocked.get.mock.calls).toHaveLength(before);
});
test('dirty route navigation uses the existing confirmation and can stay on the dashboard', async () => {
open(); const editor = await edit(); fireEvent.click(within(editor).getByRole('checkbox', { name: 'Job tags' }));
fireEvent.click(screen.getByRole('link', { name: 'Tracked jobs: 2' }));
const dialog = await screen.findByRole('dialog'); fireEvent.click(within(dialog).getByRole('button', { name: 'Keep editing' }));
expect(await screen.findByRole('region', { name: 'Editing dashboard' })).toBeVisible();
expect(mocked.put).not.toHaveBeenCalled();
});
test('navigation waits for an in-flight save instead of promising to discard a possible commit', async () => {
open(); const editor = await edit(); fireEvent.click(within(editor).getByRole('checkbox', { name: 'Job tags' }));
let complete!: (value: any) => void;
mocked.put.mockImplementationOnce((_url: string, body: any) => new Promise(resolve => { complete = resolve; stored = { ...body, revision: 1 }; }) as any);
fireEvent.click(within(editor).getByRole('button', { name: 'Save layout' }));
fireEvent.click(screen.getByRole('link', { name: 'Tracked jobs: 2' }));
expect(await screen.findByText('Please wait for your layout to finish saving before leaving.')).toBeVisible();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByText('Jobs destination')).not.toBeInTheDocument();
await act(async () => { complete({ data: stored }); });
expect(await screen.findByRole('button', { name: 'Customize dashboard' })).toBeVisible();
});