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(); }); });