import React from 'react';
import {act, renderHook} from '@testing-library/react';
import {render, screen} from '../test-utils';
import {usePages, useDriveTemperature, useDriveHealth} from './customHooks';
import {QueryContext, StatusContext} from '../contexts/contexts';
import {Paginator} from '../components/Common';
// Build a wrapper that provides a controlled QueryContext so usePages can read
// `o` and `l` from searchParams without a real router.
const makeWrapper = (initialParams = {}) => {
const params = new URLSearchParams(initialParams);
const Wrapper = ({children}) => {
const [searchParams, setSearchParams] = React.useState(params);
const updateQuery = (newParams) => {
const next = new URLSearchParams(searchParams.toString());
Object.entries(newParams).forEach(([k, v]) => {
if (v === null || v === undefined) {
next.delete(k);
} else {
next.set(k, String(v));
}
});
setSearchParams(next);
};
return (
'/'}}>
{children}
);
};
return Wrapper;
};
// Mock Media so Paginator renders both mobile + tablet variants synchronously.
jest.mock('../contexts/contexts', () => {
const actual = jest.requireActual('../contexts/contexts');
return {
...actual,
Media: ({children}) => <>{children}>,
};
});
describe('usePages.setTotal -> totalPages', () => {
// These cases describe the contract: how many pages should the Paginator
// render given a total result count and a per-page limit?
const cases = [
// [total, limit, expectedPages, label]
[0, 24, 1, 'no results -> 1 page'],
[1, 24, 1, 'fewer than one page'],
[23, 24, 1, 'just under one page'],
[24, 24, 1, 'exactly one page (BUG: reports 2)'],
[25, 24, 2, 'one full page plus one'],
[47, 24, 2, 'just under two pages'],
[48, 24, 2, 'exactly two pages (BUG: reports 3)'],
[49, 24, 3, 'just over two pages'],
[72, 24, 3, 'exactly three pages (BUG: reports 4)'],
[100, 24, 5, 'partial last page'],
[144, 24, 6, 'exactly six pages (BUG: reports 7)'],
];
test.each(cases)('total=%i limit=%i -> %i pages (%s)', (total, limit, expected) => {
const wrapper = makeWrapper({l: String(limit)});
const {result} = renderHook(() => usePages(limit), {wrapper});
act(() => {
result.current.setTotal(total);
});
expect(result.current.totalPages).toBe(expected);
});
test('the last reported page yields an offset within total (no empty trailing page)', () => {
// This is the user-visible symptom: clicking the last page link should
// produce an offset that the API can still satisfy. When totalPages is
// off-by-one, the last page maps to offset === total (or > total) and
// the API returns zero rows.
const total = 48;
const limit = 24;
const wrapper = makeWrapper({l: String(limit)});
const {result} = renderHook(() => usePages(limit), {wrapper});
act(() => {
result.current.setTotal(total);
});
act(() => {
result.current.setPage(result.current.totalPages);
});
// setPage stores (page - 1) * limit as `o` in the query.
const lastOffset = (result.current.totalPages - 1) * limit;
expect(lastOffset).toBeLessThan(total);
});
});
describe('useDriveTemperature', () => {
const makeStatusWrapper = (status) => ({children}) => (
{children}
);
test('selects the hottest drive and reads thresholds from the payload', () => {
const status = {
smart_stats: {
drives: [
{device: 'sda', temperature: 51},
{device: 'sdb', temperature: 60},
],
high_temperature: 55,
critical_temperature: 65,
},
};
const {result} = renderHook(() => useDriveTemperature(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.device).toBe('sdb');
expect(result.current.temperature).toBe(60);
expect(result.current.highTemperature).toBe(55);
expect(result.current.criticalTemperature).toBe(65);
});
test('ignores drives without a numeric temperature', () => {
const status = {
smart_stats: {
drives: [
{device: 'sda', temperature: null},
{device: 'sdb', temperature: 48},
],
high_temperature: 55,
critical_temperature: 65,
},
};
const {result} = renderHook(() => useDriveTemperature(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.device).toBe('sdb');
expect(result.current.temperature).toBe(48);
});
test('defaults to no warning and default thresholds when smart_stats is absent', () => {
const {result} = renderHook(() => useDriveTemperature(),
{wrapper: makeStatusWrapper({})});
expect(result.current.device).toBeNull();
expect(result.current.temperature).toBe(0);
expect(result.current.highTemperature).toBe(55);
expect(result.current.criticalTemperature).toBe(65);
});
test('returns no drive when none report a temperature', () => {
const status = {
smart_stats: {
drives: [{device: 'sda', temperature: null}],
high_temperature: 55,
critical_temperature: 65,
},
};
const {result} = renderHook(() => useDriveTemperature(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.device).toBeNull();
expect(result.current.temperature).toBe(0);
});
});
describe('useDriveHealth', () => {
const makeStatusWrapper = (status) => ({children}) => (
{children}
);
test('flags drives whose SMART assessment is FAIL', () => {
const status = {
smart_stats: {
drives: [
{device: 'sda', assessment: 'PASS'},
{device: 'sdb', assessment: 'FAIL'},
],
},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failing).toBe(true);
expect(result.current.failingDevices).toEqual(['sdb']);
});
test('reports every failing drive', () => {
const status = {
smart_stats: {
drives: [
{device: 'sda', assessment: 'FAIL'},
{device: 'sdb', assessment: 'FAIL'},
],
},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failingDevices).toEqual(['sda', 'sdb']);
});
test('does not warn when all drives PASS', () => {
const status = {
smart_stats: {drives: [{device: 'sda', assessment: 'PASS'}]},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failing).toBe(false);
expect(result.current.failingDevices).toEqual([]);
});
test('ignores drives with an unknown (null) assessment', () => {
const status = {
smart_stats: {drives: [{device: 'sda', assessment: null}]},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failing).toBe(false);
});
test('does not warn when smart_stats is absent', () => {
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper({})});
expect(result.current.failing).toBe(false);
expect(result.current.failingDevices).toEqual([]);
});
test('surfaces WARN drives separately from FAIL', () => {
const status = {
smart_stats: {
drives: [
{device: 'sda', health: 'PASS', assessment: 'PASS'},
{device: 'sdb', health: 'WARN', assessment: 'PASS'},
{device: 'sdc', health: 'FAIL', assessment: 'FAIL'},
],
},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failingDevices).toEqual(['sdc']);
expect(result.current.warning).toBe(true);
expect(result.current.warningDevices).toEqual(['sdb']);
});
test('prefers derived health over raw assessment', () => {
// A drive self-assessing PASS but with pending sectors is WARN.
const status = {
smart_stats: {drives: [{device: 'sda', health: 'WARN', assessment: 'PASS'}]},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failing).toBe(false);
expect(result.current.warningDevices).toEqual(['sda']);
});
test('falls back to assessment when health is absent', () => {
const status = {
smart_stats: {drives: [{device: 'sda', assessment: 'FAIL'}]},
};
const {result} = renderHook(() => useDriveHealth(),
{wrapper: makeStatusWrapper(status)});
expect(result.current.failingDevices).toEqual(['sda']);
});
});
describe('Paginator rendering', () => {
test('renders exactly totalPages numbered page links (no phantom trailing link)', () => {
// 48 results at 24 per page -> 2 pages. If totalPages is 3, the pagination
// will render an extra "3" link that fetches an empty page.
render(
{}}/>
);
// Page numbers render as elements carrying the page text.
// Tablet + mobile variants both render here because Media is mocked to
// pass children through, so each numeric page appears at least once.
expect(screen.queryAllByText('1').length).toBeGreaterThan(0);
expect(screen.queryAllByText('2').length).toBeGreaterThan(0);
expect(screen.queryByText('3')).toBeNull();
});
});