mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
The library has been uninstalled for some time, but the codebase still read as though it were there: identifiers named after it, rules written against its class names, and about 180 comments explaining a rule in terms of a thing that is no longer installed. Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap -> navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames -> iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors -> NavColors in wrolpi/schema.py, which is what validates nav_color on the way in. ColorToSemanticHexColor had no callers at all and is gone. The baseline ratchet has done its job -- the list reached zero, so the JSON is deleted and the guard keeps only the checks with teeth: no package, no import, no `className="ui "`, no toast library. Its Theme.tsx check went with the wrappers it was watching for. It also asserts its own walk found files, so a clean result cannot mean it scanned nothing. Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or `.modal`, none of which any component has set since the stylesheet stopped loading, plus the form skeletons, ui-alerts and progress-text rules that nothing renders. Two of them were doing harm rather than nothing: the drag tints had a dark-theme variant under `.inverted` that stopped applying, leaving 15% of a fixed blue over a dark row, and now mix a token instead. Two selectors naming markup that no longer exists were live bugs. The file browser's filter had `.ui.input.file-browser-filter`, so it rendered at the browser default width on a bar laid out to give it 34em -- now qualified by its container, because `.wrolpi-action-input` sets `width: 100%` at the same specificity and is loaded later. And SearchBox takes named props rather than a rest spread, so the `id` DestinationForm hands it was dropped in silence and the label it renders pointed at nothing; the download forms' Destination field has had no associated label since the migration. "Semantic roles" survives in four places, where it means what it says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
280 lines
11 KiB
JavaScript
280 lines
11 KiB
JavaScript
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 (
|
|
<QueryContext.Provider value={{searchParams, updateQuery, getLocationStr: () => '/'}}>
|
|
{children}
|
|
</QueryContext.Provider>
|
|
);
|
|
};
|
|
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}) => (
|
|
<StatusContext.Provider value={{status}}>
|
|
{children}
|
|
</StatusContext.Provider>
|
|
);
|
|
|
|
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}) => (
|
|
<StatusContext.Provider value={{status}}>
|
|
{children}
|
|
</StatusContext.Provider>
|
|
);
|
|
|
|
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(
|
|
<Paginator activePage={1} totalPages={2} onPageChange={() => {}}/>
|
|
);
|
|
|
|
// Page numbers render as <a> 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();
|
|
});
|
|
});
|