Items 1-5 of the agreed frontend upgrades. The one-time Prettier reformatting of files not touched here is in the FOLLOW-UP commit so this one stays reviewable. Testing (vitest + @testing-library) - vitest 3 + @testing-library/react + jsdom. test / test:watch / test:ui scripts; setup.ts stubs matchMedia + ResizeObserver for AntD under jsdom. - 15 tests, 4 files: RequireRole, LoginPage, measurements API client, extractApiError. Route-level code splitting - Every page converted to React.lazy() behind a Suspense boundary in App.tsx. Main bundle 1490 KB -> 714 KB; ECharts isolated in the DashboardPage chunk and only fetched when a dashboard is opened. Global 401 handling - api/client.ts response interceptor redirects any 401 from a non-/auth path to /login?returnTo=<current>; LoginPage honours the param. - Shared extractApiError replaces four near-identical per-page copies (UsersPage, AdminCustomersPage, MyProfilePage, TariffDrawer). Query devtools - @tanstack/react-query-devtools, lazy + import.meta.env.DEV-gated so a production build dead-code-eliminates it. Verified: no devtools code in any shipped .js chunk. ESLint + Prettier - ESLint 9 flat config (js + typescript-eslint + react-hooks + react-refresh, eslint-config-prettier last). Prettier config tuned to the existing style. lint / lint:fix / format / format:check scripts + lint-staged config. - Fixed one real react-hooks/exhaustive-deps warning (useMemo on DashboardsPage's `dashboards`). - End state: eslint 0 problems, tsc clean, 15/15 tests pass. Pre-commit hook (lint-staged) is installed locally in .git/hooks/ -- not version-controlled, see follow-up note. Husky was deliberately not used: it needs a core.hooksPath change and the monorepo layout makes it awkward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.4 KiB
TypeScript
82 lines
2.4 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
import { api } from './client';
|
|
import { fetchRawMeasurements, downloadRawMeasurementsExport } from './measurements';
|
|
|
|
vi.mock('./client', () => ({
|
|
api: {
|
|
get: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('measurements API client', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(api.get).mockReset();
|
|
});
|
|
|
|
it('passes the device-id filter as a comma-separated string', async () => {
|
|
vi.mocked(api.get).mockResolvedValueOnce({
|
|
data: { totalCount: 0, limit: 200, offset: 0, rows: [] },
|
|
});
|
|
await fetchRawMeasurements({
|
|
fromUtc: '2026-05-01T00:00:00Z',
|
|
toUtc: '2026-05-02T00:00:00Z',
|
|
deviceIds: ['aaa', 'bbb', 'ccc'],
|
|
limit: 200,
|
|
offset: 0,
|
|
});
|
|
expect(api.get).toHaveBeenCalledWith(
|
|
'/measurements/raw',
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({ deviceIds: 'aaa,bbb,ccc' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('omits the device-id filter when the list is empty', async () => {
|
|
vi.mocked(api.get).mockResolvedValueOnce({
|
|
data: { totalCount: 0, limit: 200, offset: 0, rows: [] },
|
|
});
|
|
await fetchRawMeasurements({
|
|
fromUtc: '2026-05-01T00:00:00Z',
|
|
toUtc: '2026-05-02T00:00:00Z',
|
|
deviceIds: [],
|
|
limit: 200,
|
|
offset: 0,
|
|
});
|
|
const call = vi.mocked(api.get).mock.calls[0][1] as { params: Record<string, unknown> };
|
|
expect(call.params.deviceIds).toBeUndefined();
|
|
});
|
|
|
|
it('builds the download URL with the full filter set', () => {
|
|
const originalLocation = window.location;
|
|
const setHref = vi.fn();
|
|
Object.defineProperty(window, 'location', {
|
|
writable: true,
|
|
value: {
|
|
...originalLocation,
|
|
set href(v: string) {
|
|
setHref(v);
|
|
},
|
|
},
|
|
});
|
|
|
|
downloadRawMeasurementsExport({
|
|
fromUtc: '2026-05-01T00:00:00.000Z',
|
|
toUtc: '2026-05-02T00:00:00.000Z',
|
|
deviceIds: ['device-1', 'device-2'],
|
|
rowCap: 50000,
|
|
});
|
|
|
|
expect(setHref).toHaveBeenCalledWith(
|
|
expect.stringContaining('/api/measurements/raw/export.xlsx?'),
|
|
);
|
|
const url = setHref.mock.calls[0][0] as string;
|
|
expect(url).toContain('from=2026-05-01T00%3A00%3A00.000Z');
|
|
expect(url).toContain('to=2026-05-02T00%3A00%3A00.000Z');
|
|
expect(url).toContain('deviceIds=device-1%2Cdevice-2');
|
|
expect(url).toContain('rowCap=50000');
|
|
|
|
Object.defineProperty(window, 'location', { writable: true, value: originalLocation });
|
|
});
|
|
});
|