skip the very first search on opening the app

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2025-12-09 10:09:18 +01:00
parent cecaa124bb
commit 8947a4eb82
7 changed files with 66 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-react': patch
---
Skip the very first empty search when going to the landing page
@@ -69,7 +69,7 @@ describe('SearchPagination', () => {
expect(screen.getByText('Results per page:')).toBeInTheDocument();
expect(screen.getByText('25')).toBeInTheDocument();
expect(screen.getByText('1-25')).toBeInTheDocument();
expect(screen.getByLabelText('Next page')).toBeEnabled();
expect(screen.getByLabelText('Next page')).toBeDisabled();
expect(screen.getByLabelText('Previous page')).toBeDisabled();
});
@@ -176,6 +176,12 @@ describe('SearchPagination', () => {
});
it('Set page limit in the context', async () => {
const initialState = {
term: 'a',
types: [],
filters: {},
};
await renderInTestApp(
<TestApiProvider
apis={[
@@ -183,7 +189,7 @@ describe('SearchPagination', () => {
[configApiRef, configApiMock],
]}
>
<SearchContextProvider>
<SearchContextProvider initialState={initialState}>
<SearchPagination />
</SearchContextProvider>
</TestApiProvider>,
@@ -205,7 +211,7 @@ describe('SearchPagination', () => {
it('Set page cursor in the context', async () => {
const initialState = {
term: '',
term: 'a',
types: [],
filters: {},
pageCursor: 'MQ==', // page: 1
@@ -253,7 +259,7 @@ describe('SearchPagination', () => {
it('Resets page cursor when page limit changes', async () => {
const initialState = {
term: '',
term: 'a',
types: [],
filters: {},
pageCursor: 'Mg==', // page: 2
@@ -280,9 +286,7 @@ describe('SearchPagination', () => {
pageCursor: undefined,
pageLimit: 10,
}),
{
signal: expect.any(AbortSignal),
},
{ signal: expect.any(AbortSignal) },
);
});
});
@@ -66,7 +66,7 @@ describe('SearchResultGroup', () => {
});
it('Renders without exploding', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -96,7 +96,7 @@ describe('SearchResultGroup', () => {
});
it('Renders search results from context', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -107,7 +107,9 @@ describe('SearchResultGroup', () => {
[analyticsApiRef, analyticsApiMock],
]}
>
<SearchContextProvider>
<SearchContextProvider
initialState={{ term: '', filters: {}, types: ['techdocs'] }}
>
<SearchResultGroup
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
@@ -128,7 +130,7 @@ describe('SearchResultGroup', () => {
});
it('Renders search results using extensions', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -166,7 +168,7 @@ describe('SearchResultGroup', () => {
});
it('Defines a default link', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -190,7 +192,7 @@ describe('SearchResultGroup', () => {
});
it('Defines a default render result item', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -221,6 +223,10 @@ describe('SearchResultGroup', () => {
});
it('Could be customized with no results text', async () => {
query.mockResolvedValue({
results: [],
});
await renderInTestApp(
<TestApiProvider
apis={[
@@ -242,7 +248,7 @@ describe('SearchResultGroup', () => {
});
it('Could be customized with filters', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -272,7 +278,7 @@ describe('SearchResultGroup', () => {
});
it('Could have a text search filter field', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -323,7 +329,7 @@ describe('SearchResultGroup', () => {
});
it('Could have a select search filter field', async () => {
query.mockResolvedValueOnce({
query.mockResolvedValue({
results,
});
@@ -376,7 +382,7 @@ describe('SearchResultGroup', () => {
});
it('Shows a progress bar when loading results', async () => {
query.mockReturnValueOnce(new Promise(() => {}));
query.mockReturnValue(new Promise(() => {}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -398,7 +404,7 @@ describe('SearchResultGroup', () => {
});
it('Does not render result group if no results returned and disableRenderingWithNoResults prop is provided', async () => {
query.mockResolvedValueOnce({ results: [] });
query.mockResolvedValue({ results: [] });
await renderInTestApp(
<TestApiProvider
apis={[
@@ -421,7 +427,7 @@ describe('SearchResultGroup', () => {
});
it('Should render custom component when no results returned', async () => {
query.mockResolvedValueOnce({ results: [] });
query.mockResolvedValue({ results: [] });
await renderInTestApp(
<TestApiProvider
apis={[
@@ -444,7 +450,7 @@ describe('SearchResultGroup', () => {
});
it('Shows an error panel when results rendering fails', async () => {
query.mockRejectedValueOnce(new Error());
query.mockRejectedValue(new Error());
await renderInTestApp(
<TestApiProvider
apis={[
@@ -130,12 +130,24 @@ const useSearchContextValue = (
const [pageCursor, setPageCursor] = useState<string | undefined>(
initialValue.pageCursor,
);
const isFirstEmptyMount = useRef(true);
const prevTerm = usePrevious(term);
const prevFilters = usePrevious(filters);
const abortControllerRef = useRef<AbortController | null>(null);
const result = useAsync(async () => {
const result = useAsync(async (): Promise<SearchResultSet> => {
if (isFirstEmptyMount.current) {
if (!term && !types.length && !Object.keys(filters).length) {
return {
results: [],
numberOfResults: 0,
};
}
isFirstEmptyMount.current = false;
}
// Here we cancel the previous request before making a new one
if (abortControllerRef.current) {
abortControllerRef.current.abort();
+1 -1
View File
@@ -264,8 +264,8 @@ export const searchTranslationRef: TranslationRef<
readonly 'searchType.tabs.allTitle': 'All';
readonly 'searchType.allResults': 'All Results';
readonly 'searchType.accordion.collapse': 'Collapse';
readonly 'searchType.accordion.allTitle': 'All';
readonly 'searchType.accordion.numberOfResults': '{{number}} results';
readonly 'searchType.accordion.allTitle': 'All';
readonly 'sidebarSearchModal.title': 'Search';
}
>;
@@ -46,12 +46,7 @@ describe('<HomePageSearchBar/>', () => {
},
);
expect(searchApiMock.query).toHaveBeenCalledWith(
expect.objectContaining({ term: '' }),
{
signal: expect.any(AbortSignal),
},
);
expect(searchApiMock.query).not.toHaveBeenCalled();
await userEvent.type(screen.getByLabelText('Search'), 'term{enter}');
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import userEvent from '@testing-library/user-event';
import { configApiRef } from '@backstage/core-plugin-api';
@@ -63,7 +63,6 @@ describe('SearchModal', () => {
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(searchApiMock.query).toHaveBeenCalledTimes(1);
});
it('Should use parent search context if defined', async () => {
@@ -106,15 +105,21 @@ describe('SearchModal', () => {
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(searchApiMock.query).toHaveBeenCalledWith(
{
term: '',
filters: {},
types: [],
pageCursor: undefined,
},
{ signal: expect.any(AbortSignal) },
);
const input = screen.getByLabelText<HTMLInputElement>('Search');
await userEvent.type(input, 'text');
await waitFor(() => {
expect(searchApiMock.query).toHaveBeenCalledWith(
{
term: 'text',
filters: {},
types: [],
pageCursor: undefined,
},
{ signal: expect.any(AbortSignal) },
);
});
});
it('Should render a custom Modal correctly', async () => {
@@ -146,7 +151,6 @@ describe('SearchModal', () => {
},
);
expect(searchApiMock.query).toHaveBeenCalledTimes(1);
await userEvent.keyboard('{Escape}');
expect(toggleModal).toHaveBeenCalledTimes(1);
});