wrolpi/app/src/api.errorAsEmpty.test.js

85 lines
2.7 KiB
JavaScript
Raw Normal View History

app: api helpers report failure instead of returning an empty value (#736) Six helpers turned a non-OK response into an empty value, so their consumers rendered a server error as "nothing here": * getConfigBackups -> {dates: []}: "No backups available for this config." A thrown request also left "Loading backups..." forever. * getInventoryBackups -> []: "No backups yet." * getDownloaders -> {downloaders: []} on a throw, the error body on a non-OK response (which then crashed the .map): the RSS form's Downloader select sat empty. * searchMap -> {results: []}: "No map locations found". * getMapSearchStatus -> {indexed: [], missing: []}: "No maps are installed". * flasherSearch -> [null, null], then `|| []`: "No .bin firmware found". Invariants now enforced and tested: * On a non-OK response each helper returns undefined; an empty value only ever comes from the server. * Each consumer shows an error for a failed or thrown fetch and keeps its "none" message for a confirmed empty result. The Downloader select is disabled and says it is loading or could not fetch. A failed map status is unknown, never "no maps are installed". * The map search tab loads from the first render, and a query shorter than the two characters the request needs now says so instead of "No map locations found". Archive browsers and recent tags are left as they are: a failed fetch there hides an optional picker or suggestion list without claiming anything. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:47:32 -06:00
import {
flasherSearch, getConfigBackups, getDownloaders, getInventoryBackups, getMapSearchStatus, searchMap,
} from './api';
// A helper that fails must say so. These used to turn a non-OK response into an empty value
// ([], {dates: []}, {results: []}, ...), so every consumer rendered "nothing here" for a server
// error. They now return undefined; an empty value only ever comes from the server.
jest.mock('./components/ui', () => ({
...jest.requireActual('./components/ui'),
toast: jest.fn(),
}));
const response = (ok, body = {}) => {
const r = {
ok,
status: ok ? 200 : 500,
json: async () => body,
text: async () => JSON.stringify(body),
};
r.clone = () => r;
return r;
};
let originalFetch;
beforeEach(() => {
originalFetch = global.fetch;
});
afterEach(() => {
global.fetch = originalFetch;
});
const failWith = () => {
global.fetch = jest.fn(() => Promise.resolve(response(false, {error: 'boom'})));
};
const succeedWith = (body) => {
global.fetch = jest.fn(() => Promise.resolve(response(true, body)));
};
describe('on a non-OK response, the helper returns undefined', () => {
test.each([
['getConfigBackups', () => getConfigBackups('wrolpi.yaml')],
['getInventoryBackups', () => getInventoryBackups('food')],
['getDownloaders', () => getDownloaders()],
['searchMap', () => searchMap('camp')],
['getMapSearchStatus', () => getMapSearchStatus()],
['flasherSearch', () => flasherSearch(null, null)],
])('%s', async (_name, call) => {
failWith();
expect(await call()).toBeUndefined();
});
});
describe('a confirmed empty result is still empty', () => {
test('getConfigBackups', async () => {
succeedWith({dates: []});
expect(await getConfigBackups('wrolpi.yaml')).toEqual({dates: []});
});
test('getInventoryBackups', async () => {
succeedWith({dates: []});
expect(await getInventoryBackups('food')).toEqual([]);
});
test('getDownloaders', async () => {
succeedWith({downloaders: []});
expect(await getDownloaders()).toEqual({downloaders: []});
});
test('searchMap', async () => {
succeedWith({results: [], total: 0});
expect(await searchMap('camp')).toEqual({results: [], total: 0});
});
test('getMapSearchStatus', async () => {
succeedWith({indexed: [], missing: []});
expect(await getMapSearchStatus()).toEqual({indexed: [], missing: []});
});
test('flasherSearch', async () => {
succeedWith({file_groups: [], totals: {file_groups: 0}});
expect(await flasherSearch(null, null)).toEqual([[], 0]);
});
});