mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
* app: search Other tab distinguishes pending channels from none useSearchChannels initialised channels to [] and loading to false, and only set loading inside its mount effect, so the Other tab's first render painted "No Channels" with a count of 0 before the search had run. A failed search was rendered the same way, since the hook had no catch and the API helper returns undefined on a non-OK response. Invariants now enforced and tested: * null = pending, undefined = fetch failed, [] = no channels; loading is true from the first render because the search starts on mount. * Changing tags resets to pending, and only the newest request may write state, so a slow response for previous tags is dropped. * The tab shows a loader while pending, an error on failure, and "No Channels" only once the server confirmed there are none. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * app: useSearchChannels follows its caller's tags The hook read its tag argument only as the initial state, so the Other tab never refetched when the filter modal changed the URL's tags while the tab stayed mounted; the list, including a stale "No Channels", was whatever the mount fetched. The internal tag state now follows the argument's contents, keyed on the serialized list because the caller builds a new array every render. Tests change the hook argument after a loaded result and expect a second search and a reset to pending, check that a new array with the same tags does not refetch, and keep the slow-response drop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
56 lines
1.9 KiB
JavaScript
56 lines
1.9 KiB
JavaScript
import React from 'react';
|
|
import {act, render, screen} from '../test-utils';
|
|
import {OtherSearchView} from './Search';
|
|
import {searchChannels} from '../api';
|
|
|
|
// What the search page's Other tab shows for each state of useSearchChannels. "No Channels"
|
|
// must only appear once the server has confirmed there are none.
|
|
|
|
jest.mock('../api', () => ({
|
|
...jest.requireActual('../api'),
|
|
searchChannels: jest.fn(),
|
|
}));
|
|
|
|
const NO_CHANNELS = /No Channels/;
|
|
|
|
beforeEach(() => searchChannels.mockReset());
|
|
|
|
test('pending: shows a loader, not "No Channels"', () => {
|
|
searchChannels.mockReturnValue(new Promise(() => {
|
|
}));
|
|
const {container} = render(<OtherSearchView loading={false}/>);
|
|
expect(container.querySelector('.wrolpi-loading')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_CHANNELS)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('failed: shows an error, not "No Channels"', async () => {
|
|
searchChannels.mockResolvedValue(undefined);
|
|
const spy = jest.spyOn(console, 'error').mockImplementation(() => {
|
|
});
|
|
try {
|
|
render(<OtherSearchView loading={false}/>);
|
|
await act(async () => {
|
|
});
|
|
expect(screen.getByText(/Could not fetch the channels/)).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_CHANNELS)).not.toBeInTheDocument();
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('empty: shows "No Channels"', async () => {
|
|
searchChannels.mockResolvedValue({channels: []});
|
|
render(<OtherSearchView loading={false}/>);
|
|
await act(async () => {
|
|
});
|
|
expect(screen.getByText(NO_CHANNELS)).toBeInTheDocument();
|
|
});
|
|
|
|
test('loaded: lists the channels', async () => {
|
|
searchChannels.mockResolvedValue({channels: [{id: 1, name: 'Cooking'}]});
|
|
render(<OtherSearchView loading={false}/>);
|
|
await act(async () => {
|
|
});
|
|
expect(screen.getByText('Cooking')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_CHANNELS)).not.toBeInTheDocument();
|
|
});
|