wrolpi/app/src/api.errorAsEmpty.test.js
lrnselfreliance 63ddad69e8
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

84 lines
2.7 KiB
JavaScript

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