mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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>
89 lines
4.1 KiB
JavaScript
89 lines
4.1 KiB
JavaScript
import React from "react";
|
|
import {fireEvent, render, screen, waitFor, within} from "../../test-utils";
|
|
|
|
jest.mock("../../api", () => ({
|
|
getInventoryBackups: jest.fn(),
|
|
postInventoryRestorePreview: jest.fn(),
|
|
postInventoryRestore: jest.fn(),
|
|
reimportInventories: jest.fn(),
|
|
}));
|
|
|
|
import {InventoryImportModal} from "./InventoryImportModal";
|
|
import {getInventoryBackups, postInventoryRestore, postInventoryRestorePreview, reimportInventories} from "../../api";
|
|
|
|
const renderModal = (props = {}) => {
|
|
const onChanged = jest.fn(() => Promise.resolve());
|
|
const onClose = jest.fn();
|
|
render(<InventoryImportModal open slug='food-storage' name='Food Storage'
|
|
onChanged={onChanged} onClose={onClose} {...props}/>);
|
|
return {onChanged, onClose};
|
|
};
|
|
|
|
describe('InventoryImportModal', () => {
|
|
// CRA's jest config resets mock implementations before each test, so (re)set them here.
|
|
beforeEach(() => {
|
|
getInventoryBackups.mockResolvedValue(['20260115', '20260101']);
|
|
postInventoryRestorePreview.mockResolvedValue({
|
|
mode: 'merge', add: [{id: 2, name: 'Beans'}], remove: [], unchanged: 1,
|
|
fields_change: false, current_count: 1, backup_count: 2, backup_name: 'Food Storage',
|
|
});
|
|
postInventoryRestore.mockResolvedValue({slug: 'food-storage', items: []});
|
|
reimportInventories.mockResolvedValue([{slug: 'food-storage'}]);
|
|
});
|
|
|
|
test('lists backups and applies a merge restore', async () => {
|
|
const {onChanged, onClose} = renderModal();
|
|
|
|
// Backup dates load (newest first).
|
|
const row = (await screen.findByText('2026-01-15')).closest('tr');
|
|
fireEvent.click(within(row).getByRole('button', {name: /merge/i}));
|
|
|
|
// Preview is requested for that date/mode, then the confirmation view appears.
|
|
await waitFor(() => expect(postInventoryRestorePreview)
|
|
.toHaveBeenCalledWith('food-storage', '20260115', 'merge'));
|
|
await screen.findByText(/Merge backup from 2026-01-15/);
|
|
expect(screen.getByText(/1 to add/)).toBeInTheDocument();
|
|
expect(screen.getByText('Beans')).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /apply/i}));
|
|
await waitFor(() => expect(postInventoryRestore)
|
|
.toHaveBeenCalledWith('food-storage', '20260115', 'merge'));
|
|
await waitFor(() => expect(onChanged).toHaveBeenCalled());
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
});
|
|
|
|
test('re-import from disk reloads inventories', async () => {
|
|
const {onChanged} = renderModal();
|
|
await screen.findByText('2026-01-15'); // wait for initial load to settle
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /re-import from disk/i}));
|
|
await waitFor(() => expect(reimportInventories).toHaveBeenCalled());
|
|
await waitFor(() => expect(onChanged).toHaveBeenCalled());
|
|
});
|
|
|
|
test('fetches backups for the inventory on open', async () => {
|
|
renderModal();
|
|
await waitFor(() => expect(getInventoryBackups).toHaveBeenCalledWith('food-storage'));
|
|
});
|
|
|
|
// "No backups yet" may only appear once the server confirmed there are none.
|
|
test('says there are no backups when the server returned none', async () => {
|
|
getInventoryBackups.mockResolvedValue([]);
|
|
renderModal();
|
|
expect(await screen.findByText(/No backups yet/)).toBeInTheDocument();
|
|
});
|
|
|
|
test('a failed backups fetch shows an error, not "No backups yet"', async () => {
|
|
getInventoryBackups.mockResolvedValue(undefined);
|
|
renderModal();
|
|
expect(await screen.findByText(/Could not fetch backups/)).toBeInTheDocument();
|
|
expect(screen.queryByText(/No backups yet/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('a thrown backups fetch shows an error, not a loader forever', async () => {
|
|
getInventoryBackups.mockRejectedValue(new Error('down'));
|
|
renderModal();
|
|
expect(await screen.findByText(/Could not fetch backups/)).toBeInTheDocument();
|
|
expect(screen.queryByText(/Loading backups/)).not.toBeInTheDocument();
|
|
});
|
|
});
|