/**
* Tests for the MaterialNumberStats widget (#2870).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MaterialNumberStats } from '../../components/MaterialNumberStats';
import { api } from '../../api/client';
vi.mock('../../api/client', () => ({
api: {
getMaterialNumberStats: vi.fn(),
},
}));
function renderWidget(props: { dateFrom?: string; dateTo?: string } = {}) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
,
);
}
describe('MaterialNumberStats', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders one row per material number with weights and cost', async () => {
(api.getMaterialNumberStats as ReturnType).mockResolvedValue([
{ material_number: '16', spool_count: 1, remaining_g: 500, consumed_g: 1500, cost: 45 },
{ material_number: '15', spool_count: 12, remaining_g: 9500, consumed_g: 250, cost: 5 },
]);
renderWidget();
expect(await screen.findByText('16')).toBeInTheDocument();
expect(screen.getByText('15')).toBeInTheDocument();
expect(screen.getByText('12')).toBeInTheDocument();
// >= 1 kg renders as kilograms, below stays in grams.
expect(screen.getByText('9.50 kg')).toBeInTheDocument();
expect(screen.getByText('500 g')).toBeInTheDocument();
expect(screen.getByText('EUR 45.00')).toBeInTheDocument();
});
it('shows the empty hint when no numbers are assigned', async () => {
(api.getMaterialNumberStats as ReturnType).mockResolvedValue([]);
renderWidget();
expect(await screen.findByText(/No material numbers assigned yet/)).toBeInTheDocument();
});
// A 403 from a missing INVENTORY_READ, a 500 or a dropped connection are
// not "you have not numbered your spools" — the two states must read
// differently or the user goes looking for a problem that isn't there.
it('reports an API failure as a failure, not as an empty inventory', async () => {
(api.getMaterialNumberStats as ReturnType).mockRejectedValue(new Error('403'));
renderWidget();
expect(await screen.findByText(/Could not load the material number statistics/)).toBeInTheDocument();
expect(screen.queryByText(/No material numbers assigned yet/)).not.toBeInTheDocument();
});
it('passes the dashboard timeframe to the endpoint', async () => {
(api.getMaterialNumberStats as ReturnType).mockResolvedValue([]);
renderWidget({ dateFrom: '2026-08-01', dateTo: '2026-08-31' });
await screen.findByText(/No material numbers assigned yet/);
expect(api.getMaterialNumberStats).toHaveBeenCalledWith({
dateFrom: '2026-08-01',
dateTo: '2026-08-31',
});
});
// The range is part of the query key, so the cache cannot serve a
// 30-day answer when the dashboard has moved to 90.
it('refetches on the same client when the timeframe changes', async () => {
(api.getMaterialNumberStats as ReturnType).mockResolvedValue([]);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const { rerender } = render(
,
);
await screen.findByText(/No material numbers assigned yet/);
expect(api.getMaterialNumberStats).toHaveBeenCalledTimes(1);
rerender(
,
);
await vi.waitFor(() => expect(api.getMaterialNumberStats).toHaveBeenCalledTimes(2));
expect(api.getMaterialNumberStats).toHaveBeenLastCalledWith({
dateFrom: '2026-09-01',
dateTo: undefined,
});
});
});