feat(dashboard): customise widget visibility order and size

This commit is contained in:
cesnimda
2026-10-21 21:07:37 +02:00
parent 68ee6cacb0
commit 77968da90c
9 changed files with 423 additions and 120 deletions
@@ -5,6 +5,7 @@ import { MemoryRouter, useLocation } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import { I18nProvider } from './i18n/I18nProvider';
import { api } from './api';
import { ConfirmProvider } from './confirm';
jest.mock('./components/OnboardingChecklist', () => ({ hasJobs }: { hasJobs: boolean | null }) => <><div>Setup retained</div><output aria-label="Setup job status">{hasJobs === null ? 'unknown' : String(hasJobs)}</output></>);
const mockedApi = api as jest.Mocked<typeof api>;
@@ -12,8 +13,10 @@ const stats = { total: 9, active: 8, deleted: 1, byStatus: { Applied: 3, Intervi
const overview = { funnel: [{ label: 'Applied', count: 3 }, { label: 'Interview', count: 2 }, { label: 'Offer', count: 1 }], totalActive: 8, totalResponses: 3, responseRateBySource: [{ label: 'NAV', total: 8, responses: 3, rate: 37.5 }], topCompanies: [{ companyId: 7, company: 'Nordlys AS', count: 8, responses: 3, responseRate: 37.5 }], timeInStage: [{ stage: 'Applied', medianDays: 4, count: 3 }], salaryInsights: [{ currency: 'NOK', period: 'year', count: 2, minimum: 600000, maximum: 800000, averageMidpoint: 700000 }] };
const monthly = [{ month: '2026-08', applied: 4, responses: 1 }, { month: '2026-09', applied: 2, responses: 1 }];
function Location() { return <output aria-label="Destination">{useLocation().pathname}{useLocation().search}</output>; }
function open() { return render(<I18nProvider><MemoryRouter><Location /><DashboardView /></MemoryRouter></I18nProvider>); }
function open() { return render(<I18nProvider><ConfirmProvider><MemoryRouter><Location /><DashboardView /></MemoryRouter></ConfirmProvider></I18nProvider>); }
let layout: any;
function data(url: string) {
if (url.endsWith('/preferences/dashboard')) return layout;
if (url.endsWith('/stats')) return stats;
if (url.endsWith('/analytics-overview')) return overview;
if (url.endsWith('/analytics')) return monthly;
@@ -21,7 +24,11 @@ function data(url: string) {
if (url.endsWith('/tag-trends')) return { months: ['2026-08', '2026-09'], series: [{ tag: 'TypeScript', counts: [3, 2] }] };
return [];
}
beforeEach(() => { localStorage.clear(); mockedApi.get.mockReset(); mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: data(url) }) as any); });
beforeEach(() => {
localStorage.clear(); layout = { version: 1, revision: 0, widgets: [] };
mockedApi.get.mockReset(); mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: data(url) }) as any);
mockedApi.put.mockImplementation((_url: string, body: any) => { layout = { ...body, revision: body.revision + 1 }; return Promise.resolve({ data: layout }) as any; });
});
test('[PRES-S01] exact monthly values, sample, custom statuses and separate salary group remain readable', async () => {
open();
@@ -37,11 +44,14 @@ test('[PRES-S01] exact monthly values, sample, custom statuses and separate sala
expect(screen.queryByText(/upcoming|conversion funnel|active applications/i)).not.toBeInTheDocument();
});
test('[PRES-S02] all five stored visibility flags hide their groups and survive remount', async () => {
test('[PRES-S02] all widget visibility choices save and survive remount', async () => {
let view = open(); await screen.findByText('Tracked jobs');
fireEvent.click(screen.getByRole('button', { name: 'Customize dashboard' }));
for (const item of screen.getAllByRole('menuitemcheckbox')) fireEvent.click(item);
expect(screen.getAllByRole('menuitemcheckbox').every(item => item.getAttribute('aria-checked') === 'false')).toBe(true);
const editor = await screen.findByRole('region', { name: 'Editing dashboard' });
for (const item of within(editor).getAllByRole('checkbox')) fireEvent.click(item);
expect(within(editor).getAllByRole('checkbox').every(item => !(item as HTMLInputElement).checked)).toBe(true);
fireEvent.click(within(editor).getByRole('button', { name: 'Save layout' }));
await waitFor(() => expect(screen.queryByRole('region', { name: 'Editing dashboard' })).not.toBeInTheDocument());
view.unmount(); view = open();
await screen.findByText('Your dashboard sections are hidden.');
expect(screen.queryByText('Tracked jobs')).not.toBeInTheDocument();