From be6250a9588f7438a51b031d5f0d4fdcf0ce2059 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 22 Jun 2023 12:57:49 -0700 Subject: [PATCH 01/14] feat(search): enable configuration of the Search component via instrumented config Signed-off-by: Enrico Alvarenga --- .../src/context/SearchContext.tsx | 17 ++++++-- plugins/search-react/src/context/index.tsx | 1 + plugins/search-react/src/index.ts | 1 + plugins/search/config.d.ts | 40 +++++++++++++++++++ plugins/search/package.json | 6 ++- .../src/components/SearchPage/SearchPage.tsx | 10 ++++- 6 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 plugins/search/config.d.ts diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index 94e51c0668..53192d0eac 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -95,16 +95,27 @@ export const useSearchContextCheck = () => { }; /** - * The initial state of `SearchContextProvider`. + * @public * + * Constructs an object representing the initial state default + * configuration for the SearchPage component + * + * @returns {SearchContextState} */ -const searchInitialState: SearchContextState = { +export const getSearchContextInitialStateDefaults = (): SearchContextState => ({ term: '', types: [], filters: {}, pageLimit: undefined, pageCursor: undefined, -}; +}); + +/** + * The initial state of `SearchContextProvider`. + * + */ +const searchInitialState: SearchContextState = + getSearchContextInitialStateDefaults(); const useSearchContextValue = ( initialValue: SearchContextState = searchInitialState, diff --git a/plugins/search-react/src/context/index.tsx b/plugins/search-react/src/context/index.tsx index 720af8a949..f00ba3ecfa 100644 --- a/plugins/search-react/src/context/index.tsx +++ b/plugins/search-react/src/context/index.tsx @@ -18,6 +18,7 @@ export { SearchContextProvider, useSearch, useSearchContextCheck, + getSearchContextInitialStateDefaults, } from './SearchContext'; export type { diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 8b234e3c05..250bb2e893 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -28,6 +28,7 @@ export { SearchContextProvider, useSearch, useSearchContextCheck, + getSearchContextInitialStateDefaults, } from './context'; export type { SearchContextProviderProps, diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts new file mode 100644 index 0000000000..5ba6a24870 --- /dev/null +++ b/plugins/search/config.d.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + app: { + /** + * Configuration options for the search plugin + */ + search?: { + /** + * An object representing the initial state configuration for the SearchPage + * component. By configuring and modifying the values of the initialState + * object attributes, you can customize the initial state of the search component + * and define how it behaves when it is first loaded or reset. + */ + initialState?: { + /** + * A number indicating the maximum number of results to be returned + * per page during pagination. + * + * @visibility frontend + */ + pageLimit?: number; + }; + }; + }; +} diff --git a/plugins/search/package.json b/plugins/search/package.json index 3bca411e3e..e155783fc8 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -70,6 +70,8 @@ "msw": "^1.0.0" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index 3ee444bd32..edac4d9b2f 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -21,8 +21,10 @@ import { useLocation, useOutlet } from 'react-router-dom'; import { SearchContextProvider, useSearch, + getSearchContextInitialStateDefaults, } from '@backstage/plugin-search-react'; import { JsonObject } from '@backstage/types'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; export const UrlUpdater = () => { const location = useLocation(); @@ -91,9 +93,15 @@ export const UrlUpdater = () => { */ export const SearchPage = () => { const outlet = useOutlet(); + const configApi = useApi(configApiRef); + + const initialState = { + ...getSearchContextInitialStateDefaults(), + pageLimit: configApi.getOptionalNumber('app.search.initialState.pageLimit'), + }; return ( - + {outlet} From fc89e4e5b78acf3f6d53d5715bfaea3efbaa62d6 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 22 Jun 2023 12:58:40 -0700 Subject: [PATCH 02/14] docs(search): update search plugin README Signed-off-by: Enrico Alvarenga --- plugins/search/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/search/README.md b/plugins/search/README.md index 3803b71e43..ae51c3a554 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -8,6 +8,18 @@ Development is ongoing. You can follow the progress and contribute at the Backst Run `yarn dev` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin. +### Optional Settings + +Configure the initialState of the search component in your `app-config.yaml` to defines how it behaves when it is first loaded or reset. + +```yaml +# app-config.yaml +app: + search: + initialState: + pageLimit: 50 +``` + ### Areas of Responsibility This search plugin is primarily responsible for the following: From b78f570f44d312fb810470757b721358a87749e2 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 22 Jun 2023 13:17:01 -0700 Subject: [PATCH 03/14] chore: create changeset Signed-off-by: Enrico Alvarenga --- .changeset/nasty-forks-beg.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/nasty-forks-beg.md diff --git a/.changeset/nasty-forks-beg.md b/.changeset/nasty-forks-beg.md new file mode 100644 index 0000000000..464e6418fd --- /dev/null +++ b/.changeset/nasty-forks-beg.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-search-react': minor +'@backstage/plugin-search': minor +--- + +The SearchPage component can now be configured via app-config.yaml with an initial state to define how it behaves when it is first loaded or reset. Check out the following example: + +```yaml +app: + search: + initialState: + pageLimit: 50 +``` From 47c4082a0e8fded79ef0c27ce89c7c67a94d210d Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 22 Jun 2023 13:37:14 -0700 Subject: [PATCH 04/14] fix: search plugin README typos Signed-off-by: Enrico Alvarenga --- plugins/search/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search/README.md b/plugins/search/README.md index ae51c3a554..203f295333 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -10,7 +10,7 @@ Run `yarn dev` in the root directory, and then navigate to [/search](http://loca ### Optional Settings -Configure the initialState of the search component in your `app-config.yaml` to defines how it behaves when it is first loaded or reset. +Configure the SearchPage component initial state via `app-config.yaml` to define how it behaves when it is first loaded or reset. ```yaml # app-config.yaml From a5da788797c689b42316c0819d05469520714fa4 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Thu, 22 Jun 2023 13:56:46 -0700 Subject: [PATCH 05/14] fix: api-reports Signed-off-by: Enrico Alvarenga --- plugins/search-react/api-report.md | 3 +++ plugins/search-react/src/context/SearchContext.tsx | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 97c8300c14..f938906d3f 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -61,6 +61,9 @@ export type DefaultResultListItemProps = { toggleModal?: () => void; }; +// @public +export const getSearchContextInitialStateDefaults: () => SearchContextState; + // @public (undocumented) export const HighlightedSearchResultText: ( props: HighlightedSearchResultTextProps, diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index 53192d0eac..0a4bdf283b 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -99,8 +99,6 @@ export const useSearchContextCheck = () => { * * Constructs an object representing the initial state default * configuration for the SearchPage component - * - * @returns {SearchContextState} */ export const getSearchContextInitialStateDefaults = (): SearchContextState => ({ term: '', From 19d979dc48b5ba29e9ad2d6a12b51f6fc36a5354 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Mon, 26 Jun 2023 11:29:59 -0700 Subject: [PATCH 06/14] test: unit test initial state config Signed-off-by: Enrico Alvarenga --- .../components/SearchPage/SearchPage.test.tsx | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx index be9e7f7b10..f364cb8dd4 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx @@ -14,18 +14,41 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; +import { + MockConfigApi, + TestApiProvider, + renderInTestApp, +} from '@backstage/test-utils'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useSearch } from '@backstage/plugin-search-react'; +import { + type SearchContextState, + useSearch, + getSearchContextInitialStateDefaults, +} from '@backstage/plugin-search-react'; import { SearchPage } from './SearchPage'; +import { configApiRef } from '@backstage/core-plugin-api'; +import type { JsonObject } from '@backstage/types'; + +const TestingContext = React.createContext( + getSearchContextInitialStateDefaults(), +); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn().mockReturnValue({ search: '', }), - useOutlet: jest.fn().mockReturnValue('Route Children'), + useOutlet: jest.fn().mockImplementation(() => ( + + {value => ( + <> +

Route Children

+
Page Limit: {value.pageLimit}
+ + )} +
+ )), })); const setTermMock = jest.fn(); @@ -37,7 +60,11 @@ jest.mock('@backstage/plugin-search-react', () => ({ ...jest.requireActual('@backstage/plugin-search-react'), SearchContextProvider: jest .fn() - .mockImplementation(({ children }) => children), + .mockImplementation(({ initialState, children }) => ( + + {children} + + )), useSearch: jest.fn().mockReturnValue({ term: '', setTerm: (term: any) => setTermMock(term), @@ -50,6 +77,13 @@ jest.mock('@backstage/plugin-search-react', () => ({ }), })); +const renderSearchPageWithConfig = (config: JsonObject = {}) => + renderInTestApp( + + + , + ); + describe('SearchPage', () => { const origReplaceState = window.history.replaceState; @@ -76,7 +110,7 @@ describe('SearchPage', () => { }); // When we render the page... - await renderInTestApp(); + await renderSearchPageWithConfig(); // Then search context should be set with these values... expect(setTermMock).toHaveBeenCalledWith(expectedTerm); @@ -86,7 +120,7 @@ describe('SearchPage', () => { }); it('renders provided router element', async () => { - const { getByText } = await renderInTestApp(); + const { getByText } = await renderSearchPageWithConfig(); expect(getByText('Route Children')).toBeInTheDocument(); }); @@ -106,9 +140,27 @@ describe('SearchPage', () => { '?query=bieber&types[]=software-catalog&pageCursor=SOMEPAGE&filters[anyKey]=anyValue', ); - await renderInTestApp(); + await renderSearchPageWithConfig(); const calls = (window.history.replaceState as jest.Mock).mock.calls[0]; expect(calls[2]).toContain(expectedLocation); }); + + it('initializes the SearchPage with a custom page limit', async () => { + const { getByText } = await renderSearchPageWithConfig({ + app: { + search: { + initialState: { + pageLimit: 50, + }, + }, + }, + }); + + const calls = (window.history.replaceState as jest.Mock).mock.calls[0]; + const expectedLocation = encodeURI('?query=&pageCursor='); + expect(calls[2]).toContain(expectedLocation); + + expect(getByText('Page Limit: 50')).toBeInTheDocument(); + }); }); From 0a8fda4de9d17438417c0c1730de64c6be5da6b3 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Mon, 26 Jun 2023 12:38:59 -0700 Subject: [PATCH 07/14] fix: pageLimit only accepts certain numbers Signed-off-by: Enrico Alvarenga --- .changeset/nasty-forks-beg.md | 2 ++ plugins/search/README.md | 2 ++ plugins/search/config.d.ts | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/nasty-forks-beg.md b/.changeset/nasty-forks-beg.md index 464e6418fd..e8fc1198da 100644 --- a/.changeset/nasty-forks-beg.md +++ b/.changeset/nasty-forks-beg.md @@ -11,3 +11,5 @@ app: initialState: pageLimit: 50 ``` + +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. diff --git a/plugins/search/README.md b/plugins/search/README.md index 203f295333..f7dbcd9617 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -20,6 +20,8 @@ app: pageLimit: 50 ``` +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + ### Areas of Responsibility This search plugin is primarily responsible for the following: diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts index 5ba6a24870..2c284a53b4 100644 --- a/plugins/search/config.d.ts +++ b/plugins/search/config.d.ts @@ -33,7 +33,7 @@ export interface Config { * * @visibility frontend */ - pageLimit?: number; + pageLimit?: 10 | 25 | 50 | 100; }; }; }; From 2ddf648a0187c529070f9a179ab30e6e6bc63546 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Tue, 1 Aug 2023 11:26:00 -0700 Subject: [PATCH 08/14] fix: copyright copy&paste typo Co-authored-by: Camila Belo Signed-off-by: Enrico Alvarenga --- plugins/search/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts index 2c284a53b4..ffbbd7636b 100644 --- a/plugins/search/config.d.ts +++ b/plugins/search/config.d.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 40de4a7210666555e60f2a33813499eb3bd586ea Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Wed, 2 Aug 2023 10:44:04 -0700 Subject: [PATCH 09/14] refactor: move configApi to SearchContext Signed-off-by: Enrico Alvarenga --- .../src/context/SearchContext.test.tsx | 91 ++++++++++++++----- .../src/context/SearchContext.tsx | 33 ++++--- plugins/search/config.d.ts | 8 +- .../components/SearchPage/SearchPage.test.tsx | 66 ++------------ .../src/components/SearchPage/SearchPage.tsx | 10 +- 5 files changed, 98 insertions(+), 110 deletions(-) diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index edee2f153f..139a1b2939 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -14,28 +14,32 @@ * limitations under the License. */ -import { useApi } from '@backstage/core-plugin-api'; +import { configApiRef } from '@backstage/core-plugin-api'; import { render, screen, waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SearchContextProvider, useSearch, useSearchContextCheck, } from './SearchContext'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), - useApi: jest.fn(), -})); +import { searchApiRef } from '../api'; describe('SearchContext', () => { const query = jest.fn(); - const wrapper = ({ children, initialState }: any) => ( - - {children} - + const wrapper = ({ children, initialState, config = {} }: any) => ( + + + {children} + + ); const initialState = { @@ -46,7 +50,6 @@ describe('SearchContext', () => { beforeEach(() => { query.mockResolvedValue({}); - (useApi as jest.Mock).mockReturnValue({ query: query }); }); afterAll(() => { @@ -56,11 +59,7 @@ describe('SearchContext', () => { it('Passes children', async () => { const text = 'text'; - render( - - {text} - , - ); + render(wrapper({ children: text, initialState })); await waitFor(() => { expect(screen.getByText(text)).toBeInTheDocument(); @@ -95,17 +94,61 @@ describe('SearchContext', () => { expect(result.current).toEqual(true); }); - it('Uses initial state values', async () => { - const { result, waitForNextUpdate } = renderHook(() => useSearch(), { - wrapper, - initialProps: { - initialState, - }, + describe('Uses initial state values', () => { + it('Uses default initial state values', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ + term: '', + types: [], + filters: {}, + pageLimit: undefined, + pageCursor: undefined, + }), + ); }); - await waitForNextUpdate(); + it('Uses provided initial state values', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); - expect(result.current).toEqual(expect.objectContaining(initialState)); + await waitForNextUpdate(); + + expect(result.current).toEqual(expect.objectContaining(initialState)); + }); + + it('Uses page limit provided via config api', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + config: { + app: { + search: { + query: { + pageLimit: 100, + }, + }, + }, + }, + }, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ ...initialState, pageLimit: 100 }), + ); + }); }); describe('Resets cursor', () => { diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index 0a4bdf283b..ea041d6bc1 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -30,7 +30,11 @@ import { createVersionedValueMap, } from '@backstage/version-bridge'; import { JsonObject } from '@backstage/types'; -import { AnalyticsContext, useApi } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + useApi, + configApiRef, +} from '@backstage/core-plugin-api'; import { SearchResultSet } from '@backstage/plugin-search-common'; import { searchApiRef } from '../api'; @@ -95,25 +99,16 @@ export const useSearchContextCheck = () => { }; /** - * @public + * The initial state of `SearchContextProvider`. * - * Constructs an object representing the initial state default - * configuration for the SearchPage component */ -export const getSearchContextInitialStateDefaults = (): SearchContextState => ({ +export const searchInitialState = { term: '', types: [], filters: {}, pageLimit: undefined, pageCursor: undefined, -}); - -/** - * The initial state of `SearchContextProvider`. - * - */ -const searchInitialState: SearchContextState = - getSearchContextInitialStateDefaults(); +}; const useSearchContextValue = ( initialValue: SearchContextState = searchInitialState, @@ -248,10 +243,20 @@ export const SearchContextProvider = (props: SearchContextProviderProps) => { const { initialState, inheritParentContextIfAvailable, children } = props; const hasParentContext = useSearchContextCheck(); + const configApi = useApi(configApiRef); + + const searchContextInitialState = { + ...searchInitialState, + ...(initialState || {}), + pageLimit: + configApi.getOptionalNumber('app.search.query.pageLimit') || + initialState?.pageLimit, + }; + return hasParentContext && inheritParentContextIfAvailable ? ( <>{children} ) : ( - + {children} ); diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts index ffbbd7636b..7ec00425d3 100644 --- a/plugins/search/config.d.ts +++ b/plugins/search/config.d.ts @@ -21,12 +21,12 @@ export interface Config { */ search?: { /** - * An object representing the initial state configuration for the SearchPage - * component. By configuring and modifying the values of the initialState - * object attributes, you can customize the initial state of the search component + * An object representing the default search query configuration. + * By configuring and modifying the values of this object, + * you can customize the default values of the search component * and define how it behaves when it is first loaded or reset. */ - initialState?: { + query?: { /** * A number indicating the maximum number of results to be returned * per page during pagination. diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx index f364cb8dd4..be9e7f7b10 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx @@ -14,41 +14,18 @@ * limitations under the License. */ -import { - MockConfigApi, - TestApiProvider, - renderInTestApp, -} from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import { - type SearchContextState, - useSearch, - getSearchContextInitialStateDefaults, -} from '@backstage/plugin-search-react'; +import { useSearch } from '@backstage/plugin-search-react'; import { SearchPage } from './SearchPage'; -import { configApiRef } from '@backstage/core-plugin-api'; -import type { JsonObject } from '@backstage/types'; - -const TestingContext = React.createContext( - getSearchContextInitialStateDefaults(), -); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn().mockReturnValue({ search: '', }), - useOutlet: jest.fn().mockImplementation(() => ( - - {value => ( - <> -

Route Children

-
Page Limit: {value.pageLimit}
- - )} -
- )), + useOutlet: jest.fn().mockReturnValue('Route Children'), })); const setTermMock = jest.fn(); @@ -60,11 +37,7 @@ jest.mock('@backstage/plugin-search-react', () => ({ ...jest.requireActual('@backstage/plugin-search-react'), SearchContextProvider: jest .fn() - .mockImplementation(({ initialState, children }) => ( - - {children} - - )), + .mockImplementation(({ children }) => children), useSearch: jest.fn().mockReturnValue({ term: '', setTerm: (term: any) => setTermMock(term), @@ -77,13 +50,6 @@ jest.mock('@backstage/plugin-search-react', () => ({ }), })); -const renderSearchPageWithConfig = (config: JsonObject = {}) => - renderInTestApp( - - - , - ); - describe('SearchPage', () => { const origReplaceState = window.history.replaceState; @@ -110,7 +76,7 @@ describe('SearchPage', () => { }); // When we render the page... - await renderSearchPageWithConfig(); + await renderInTestApp(); // Then search context should be set with these values... expect(setTermMock).toHaveBeenCalledWith(expectedTerm); @@ -120,7 +86,7 @@ describe('SearchPage', () => { }); it('renders provided router element', async () => { - const { getByText } = await renderSearchPageWithConfig(); + const { getByText } = await renderInTestApp(); expect(getByText('Route Children')).toBeInTheDocument(); }); @@ -140,27 +106,9 @@ describe('SearchPage', () => { '?query=bieber&types[]=software-catalog&pageCursor=SOMEPAGE&filters[anyKey]=anyValue', ); - await renderSearchPageWithConfig(); + await renderInTestApp(); const calls = (window.history.replaceState as jest.Mock).mock.calls[0]; expect(calls[2]).toContain(expectedLocation); }); - - it('initializes the SearchPage with a custom page limit', async () => { - const { getByText } = await renderSearchPageWithConfig({ - app: { - search: { - initialState: { - pageLimit: 50, - }, - }, - }, - }); - - const calls = (window.history.replaceState as jest.Mock).mock.calls[0]; - const expectedLocation = encodeURI('?query=&pageCursor='); - expect(calls[2]).toContain(expectedLocation); - - expect(getByText('Page Limit: 50')).toBeInTheDocument(); - }); }); diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index edac4d9b2f..3ee444bd32 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -21,10 +21,8 @@ import { useLocation, useOutlet } from 'react-router-dom'; import { SearchContextProvider, useSearch, - getSearchContextInitialStateDefaults, } from '@backstage/plugin-search-react'; import { JsonObject } from '@backstage/types'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; export const UrlUpdater = () => { const location = useLocation(); @@ -93,15 +91,9 @@ export const UrlUpdater = () => { */ export const SearchPage = () => { const outlet = useOutlet(); - const configApi = useApi(configApiRef); - - const initialState = { - ...getSearchContextInitialStateDefaults(), - pageLimit: configApi.getOptionalNumber('app.search.initialState.pageLimit'), - }; return ( - + {outlet} From bdf9e1d668a7eff77f4cd2f88cf1ed84b328a450 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Wed, 2 Aug 2023 13:46:39 -0700 Subject: [PATCH 10/14] refactor: move config to search-backend plugin Signed-off-by: Enrico Alvarenga --- .../uffizzi.production.app-config.yaml | 4 ++ plugins/search-backend/README.md | 16 ++++++++ plugins/search-backend/config.d.ts | 16 ++++++++ plugins/search-react/api-report.md | 3 -- .../src/context/SearchContext.tsx | 2 +- plugins/search-react/src/context/index.tsx | 1 - plugins/search-react/src/index.ts | 1 - plugins/search/README.md | 14 ------- plugins/search/config.d.ts | 40 ------------------- plugins/search/package.json | 6 +-- 10 files changed, 39 insertions(+), 64 deletions(-) delete mode 100644 plugins/search/config.d.ts diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index 4f64e5ffcb..2f84e766d7 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -280,3 +280,7 @@ gocd: permission: enabled: true + +search: + query: + pageLimit: 100 \ No newline at end of file diff --git a/plugins/search-backend/README.md b/plugins/search-backend/README.md index b89c6acf68..3c85258b2c 100644 --- a/plugins/search-backend/README.md +++ b/plugins/search-backend/README.md @@ -10,3 +10,19 @@ centralized in the `search` plugin README.md. For a better overview of how the search platform is put together, check the [Backstage Search Architecture](https://backstage.io/docs/features/search/architecture) documentation. + +### Optional Settings + +Configure the search query values via `app-config.yaml` to define how it behaves by default. + +```yaml +# app-config.yaml +search: + query: + pageLimit: 50 +``` + +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + +**NOTE**: Currently this configuration only reflects the initial state of the Search React components. This means that +it defines how it behaves when it is first loaded or reset. diff --git a/plugins/search-backend/config.d.ts b/plugins/search-backend/config.d.ts index 3dbdf78dfe..ff225be500 100644 --- a/plugins/search-backend/config.d.ts +++ b/plugins/search-backend/config.d.ts @@ -40,5 +40,21 @@ export interface Config { */ queryLatencyBudgetMs?: number; }; + + /** + * An object representing the default search query configuration. + * By configuring and modifying the values of this object, + * you can customize the default values of the search queries + * and define how it behaves by default. + */ + query?: { + /** + * A number indicating the maximum number of results to be returned + * per page during pagination. + * + * @visibility frontend + */ + pageLimit?: 10 | 25 | 50 | 100; + }; }; } diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index f938906d3f..97c8300c14 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -61,9 +61,6 @@ export type DefaultResultListItemProps = { toggleModal?: () => void; }; -// @public -export const getSearchContextInitialStateDefaults: () => SearchContextState; - // @public (undocumented) export const HighlightedSearchResultText: ( props: HighlightedSearchResultTextProps, diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index ea041d6bc1..e62aeedbc0 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -249,7 +249,7 @@ export const SearchContextProvider = (props: SearchContextProviderProps) => { ...searchInitialState, ...(initialState || {}), pageLimit: - configApi.getOptionalNumber('app.search.query.pageLimit') || + configApi.getOptionalNumber('search.query.pageLimit') || initialState?.pageLimit, }; diff --git a/plugins/search-react/src/context/index.tsx b/plugins/search-react/src/context/index.tsx index f00ba3ecfa..720af8a949 100644 --- a/plugins/search-react/src/context/index.tsx +++ b/plugins/search-react/src/context/index.tsx @@ -18,7 +18,6 @@ export { SearchContextProvider, useSearch, useSearchContextCheck, - getSearchContextInitialStateDefaults, } from './SearchContext'; export type { diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 250bb2e893..8b234e3c05 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -28,7 +28,6 @@ export { SearchContextProvider, useSearch, useSearchContextCheck, - getSearchContextInitialStateDefaults, } from './context'; export type { SearchContextProviderProps, diff --git a/plugins/search/README.md b/plugins/search/README.md index f7dbcd9617..3803b71e43 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -8,20 +8,6 @@ Development is ongoing. You can follow the progress and contribute at the Backst Run `yarn dev` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin. -### Optional Settings - -Configure the SearchPage component initial state via `app-config.yaml` to define how it behaves when it is first loaded or reset. - -```yaml -# app-config.yaml -app: - search: - initialState: - pageLimit: 50 -``` - -Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. - ### Areas of Responsibility This search plugin is primarily responsible for the following: diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts deleted file mode 100644 index 7ec00425d3..0000000000 --- a/plugins/search/config.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Config { - app: { - /** - * Configuration options for the search plugin - */ - search?: { - /** - * An object representing the default search query configuration. - * By configuring and modifying the values of this object, - * you can customize the default values of the search component - * and define how it behaves when it is first loaded or reset. - */ - query?: { - /** - * A number indicating the maximum number of results to be returned - * per page during pagination. - * - * @visibility frontend - */ - pageLimit?: 10 | 25 | 50 | 100; - }; - }; - }; -} diff --git a/plugins/search/package.json b/plugins/search/package.json index e155783fc8..3bca411e3e 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -70,8 +70,6 @@ "msw": "^1.0.0" }, "files": [ - "dist", - "config.d.ts" - ], - "configSchema": "config.d.ts" + "dist" + ] } From 18aa9d27ecb9a676799b08f7b505c0e8ad817624 Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Wed, 2 Aug 2023 13:48:26 -0700 Subject: [PATCH 11/14] test: SearchContext uses pageLimit from config Signed-off-by: Enrico Alvarenga --- plugins/search-react/src/context/SearchContext.test.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 139a1b2939..263dab4bfa 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -132,11 +132,9 @@ describe('SearchContext', () => { initialProps: { initialState, config: { - app: { - search: { - query: { - pageLimit: 100, - }, + search: { + query: { + pageLimit: 100, }, }, }, From 9f09a702d68268c25b605fd6d2b570f547155daa Mon Sep 17 00:00:00 2001 From: Enrico Alvarenga Date: Wed, 2 Aug 2023 14:19:54 -0700 Subject: [PATCH 12/14] revert: move config back to search plugin Signed-off-by: Enrico Alvarenga --- .changeset/nasty-forks-beg.md | 9 +++-- .../uffizzi.production.app-config.yaml | 2 +- plugins/search-backend/README.md | 16 --------- plugins/search-backend/config.d.ts | 16 --------- plugins/search-backend/package.json | 3 +- .../src/context/SearchContext.tsx | 2 +- plugins/search/README.md | 16 +++++++++ plugins/search/config.d.ts | 35 +++++++++++++++++++ plugins/search/package.json | 6 ++-- 9 files changed, 62 insertions(+), 43 deletions(-) create mode 100644 plugins/search/config.d.ts diff --git a/.changeset/nasty-forks-beg.md b/.changeset/nasty-forks-beg.md index e8fc1198da..0662431453 100644 --- a/.changeset/nasty-forks-beg.md +++ b/.changeset/nasty-forks-beg.md @@ -3,13 +3,12 @@ '@backstage/plugin-search': minor --- -The SearchPage component can now be configured via app-config.yaml with an initial state to define how it behaves when it is first loaded or reset. Check out the following example: +The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: ```yaml -app: - search: - initialState: - pageLimit: 50 +search: + query: + pageLimit: 50 ``` Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index 2f84e766d7..34f4c90f0b 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -283,4 +283,4 @@ permission: search: query: - pageLimit: 100 \ No newline at end of file + pageLimit: 50 diff --git a/plugins/search-backend/README.md b/plugins/search-backend/README.md index 3c85258b2c..b89c6acf68 100644 --- a/plugins/search-backend/README.md +++ b/plugins/search-backend/README.md @@ -10,19 +10,3 @@ centralized in the `search` plugin README.md. For a better overview of how the search platform is put together, check the [Backstage Search Architecture](https://backstage.io/docs/features/search/architecture) documentation. - -### Optional Settings - -Configure the search query values via `app-config.yaml` to define how it behaves by default. - -```yaml -# app-config.yaml -search: - query: - pageLimit: 50 -``` - -Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. - -**NOTE**: Currently this configuration only reflects the initial state of the Search React components. This means that -it defines how it behaves when it is first loaded or reset. diff --git a/plugins/search-backend/config.d.ts b/plugins/search-backend/config.d.ts index ff225be500..3dbdf78dfe 100644 --- a/plugins/search-backend/config.d.ts +++ b/plugins/search-backend/config.d.ts @@ -40,21 +40,5 @@ export interface Config { */ queryLatencyBudgetMs?: number; }; - - /** - * An object representing the default search query configuration. - * By configuring and modifying the values of this object, - * you can customize the default values of the search queries - * and define how it behaves by default. - */ - query?: { - /** - * A number indicating the maximum number of results to be returned - * per page during pagination. - * - * @visibility frontend - */ - pageLimit?: 10 | 25 | 50 | 100; - }; }; } diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index ba36bea2a9..d46197735f 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -72,6 +72,5 @@ "files": [ "dist", "config.d.ts" - ], - "configSchema": "config.d.ts" + ] } diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index e62aeedbc0..add62b39df 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -102,7 +102,7 @@ export const useSearchContextCheck = () => { * The initial state of `SearchContextProvider`. * */ -export const searchInitialState = { +const searchInitialState: SearchContextState = { term: '', types: [], filters: {}, diff --git a/plugins/search/README.md b/plugins/search/README.md index 3803b71e43..e6b664338d 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -8,6 +8,22 @@ Development is ongoing. You can follow the progress and contribute at the Backst Run `yarn dev` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin. +### Optional Settings + +Configure the search query values via `app-config.yaml` to define how it behaves by default. + +```yaml +# app-config.yaml +search: + query: + pageLimit: 50 +``` + +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + +**NOTE**: Currently this configuration only reflects the initial state of the Search React components. This means that +it defines how it behaves when it is first loaded or reset. + ### Areas of Responsibility This search plugin is primarily responsible for the following: diff --git a/plugins/search/config.d.ts b/plugins/search/config.d.ts new file mode 100644 index 0000000000..d9a5ce4a1b --- /dev/null +++ b/plugins/search/config.d.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Configuration options for the search plugin */ + search?: { + /** + * An object representing the default search query configuration. + * By configuring and modifying the values of this object, + * you can customize the default values of the search queries + * and define how it behaves by default. + */ + query?: { + /** + * A number indicating the maximum number of results to be returned + * per page during pagination. + * @visibility frontend + */ + pageLimit?: 10 | 25 | 50 | 100; + }; + }; +} diff --git a/plugins/search/package.json b/plugins/search/package.json index 3bca411e3e..e155783fc8 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -70,6 +70,8 @@ "msw": "^1.0.0" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } From ccad82226db966a57ae8df9d90df595a47617b0a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 22 Aug 2023 09:55:29 +0200 Subject: [PATCH 13/14] test(search): mock config api ref Signed-off-by: Camila Belo --- .../components/SearchBar/SearchBar.test.tsx | 14 +- .../SearchFilter.Autocomplete.test.tsx | 84 +++++- .../SearchFilter/SearchFilter.test.tsx | 239 ++++++++++++------ .../components/SearchFilter/hooks.test.tsx | 19 +- .../SearchPagination.test.tsx | 100 ++++++-- .../src/context/SearchContext.test.tsx | 49 ++-- .../SearchModal/SearchModal.test.tsx | 21 +- .../SearchType/SearchType.Accordion.test.tsx | 29 ++- .../SearchType/SearchType.Tabs.test.tsx | 19 +- .../components/SearchType/SearchType.test.tsx | 113 ++++++--- 10 files changed, 488 insertions(+), 199 deletions(-) diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 9b4664aceb..ce3c285852 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -43,10 +43,6 @@ const createInitialState = ({ describe('SearchBar', () => { const user = userEvent.setup({ delay: null }); - const query = jest.fn().mockResolvedValue({ results: [] }); - - const searchApiMock = { query }; - const configApiMock = new ConfigReader({ app: { title: 'Mock title', @@ -58,6 +54,8 @@ describe('SearchBar', () => { }, }); + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; + beforeEach(() => { jest.clearAllMocks(); }); @@ -172,7 +170,7 @@ describe('SearchBar', () => { await waitFor(() => expect(textbox).toHaveValue(value)); - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); @@ -204,7 +202,7 @@ describe('SearchBar', () => { await waitFor(() => expect(textbox).toHaveValue('')); - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: '' }), ); }); @@ -255,7 +253,7 @@ describe('SearchBar', () => { await user.type(textbox, value); await waitFor(() => - expect(query).not.toHaveBeenLastCalledWith( + expect(searchApiMock.query).not.toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ), ); @@ -266,7 +264,7 @@ describe('SearchBar', () => { await waitFor(() => expect(textbox).toHaveValue(value)); - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx index 14c8967b57..765d2a26d1 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TestApiProvider } from '@backstage/test-utils'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { screen, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -22,6 +22,7 @@ import React from 'react'; import { searchApiRef } from '../../api'; import { SearchContextProvider, useSearch } from '../../context'; import { SearchFilter } from './SearchFilter'; +import { configApiRef } from '@backstage/core-plugin-api'; const SearchContextFilterSpy = ({ name }: { name: string }) => { const { filters } = useSearch(); @@ -34,7 +35,16 @@ const SearchContextFilterSpy = ({ name }: { name: string }) => { }; describe('SearchFilter.Autocomplete', () => { - const query = jest.fn().mockResolvedValue({}); + const configApiMock = new MockConfigApi({ + search: { + query: { + pageLimit: 100, + }, + }, + }); + + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; + const emptySearchContext = { term: '', types: [], @@ -46,7 +56,12 @@ describe('SearchFilter.Autocomplete', () => { it('renders as expected', async () => { render( - + @@ -67,7 +82,12 @@ describe('SearchFilter.Autocomplete', () => { it('renders as expected with async values', async () => { render( - + values} /> @@ -88,7 +108,12 @@ describe('SearchFilter.Autocomplete', () => { it('does not affect unrelated filter state', async () => { render( - + { describe('single', () => { it('renders as expected with defaultValue', async () => { render( - + { it('renders as expected with initial context', async () => { render( - + { it('sets filter state when selecting a value', async () => { render( - + @@ -221,7 +261,12 @@ describe('SearchFilter.Autocomplete', () => { describe('multiple', () => { it('renders as expected with defaultValue', async () => { render( - + { it('renders as expected with initial context', async () => { render( - + { it('respects tag limit configuration', async () => { render( - + { it('sets filter state when selecting a value', async () => { render( - + diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx index 89a1818e8d..c51a4832e3 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -18,15 +18,12 @@ import React from 'react'; import { screen, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useApi } from '@backstage/core-plugin-api'; +import { configApiRef } from '@backstage/core-plugin-api'; import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), - useApi: jest.fn().mockReturnValue({}), -})); +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { searchApiRef } from '../../api'; describe('SearchFilter', () => { const initialState = { @@ -40,8 +37,15 @@ describe('SearchFilter', () => { const values = ['value1', 'value2']; const filters = { unrelated: 'unrelated' }; - const query = jest.fn().mockResolvedValue({}); - (useApi as jest.Mock).mockReturnValue({ query: query }); + const configApiMock = new MockConfigApi({ + search: { + query: { + pagelimit: 10, + }, + }, + }); + + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; afterAll(() => { jest.resetAllMocks(); @@ -58,9 +62,16 @@ describe('SearchFilter', () => { describe('Checkbox', () => { it('Renders field name and values when provided as props', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -77,16 +88,23 @@ describe('SearchFilter', () => { it('Renders correctly based on filter state', async () => { render( - - - , + + + + , ); await waitFor(() => { @@ -101,14 +119,21 @@ describe('SearchFilter', () => { it('Renders correctly based on defaultValue', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -123,9 +148,16 @@ describe('SearchFilter', () => { it('Checking / unchecking a value sets filter state', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -137,7 +169,7 @@ describe('SearchFilter', () => { // Check the box. await userEvent.click(checkBox); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: { field: [values[0]] } }), ); }); @@ -145,7 +177,7 @@ describe('SearchFilter', () => { // Uncheck the box. await userEvent.click(checkBox); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: {} }), ); }); @@ -153,9 +185,16 @@ describe('SearchFilter', () => { it('Checking / unchecking a value maintains unrelated filter state', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -167,7 +206,7 @@ describe('SearchFilter', () => { // Check the box. await userEvent.click(checkBox); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: { ...filters, field: [values[0]] }, }), @@ -177,7 +216,7 @@ describe('SearchFilter', () => { // Uncheck the box. await userEvent.click(checkBox); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters }), ); }); @@ -187,9 +226,16 @@ describe('SearchFilter', () => { describe('Select', () => { it('Renders field name and values when provided as props', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -212,13 +258,20 @@ describe('SearchFilter', () => { it('Renders values when provided asynchronously', async () => { render( - - values} - /> - , + + + values} + /> + + , ); await waitFor(() => { @@ -244,16 +297,23 @@ describe('SearchFilter', () => { it('Renders correctly based on filter state', async () => { render( - - - , + + + + , ); await waitFor(() => { @@ -280,14 +340,21 @@ describe('SearchFilter', () => { it('Renders correctly based on defaultValue', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -314,9 +381,16 @@ describe('SearchFilter', () => { it('Selecting a value sets filter state', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -334,7 +408,7 @@ describe('SearchFilter', () => { await userEvent.click(screen.getByRole('option', { name: values[0] })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: { [name]: values[0] }, }), @@ -350,7 +424,7 @@ describe('SearchFilter', () => { await userEvent.click(screen.getByRole('option', { name: 'All' })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: {}, }), @@ -360,14 +434,21 @@ describe('SearchFilter', () => { it('Selecting a value maintains unrelated filter state', async () => { render( - - - , + + + + , ); await waitFor(() => { @@ -385,7 +466,7 @@ describe('SearchFilter', () => { await userEvent.click(screen.getByRole('option', { name: values[0] })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: { ...filters, [name]: values[0] }, }), @@ -401,7 +482,7 @@ describe('SearchFilter', () => { await userEvent.click(screen.getByRole('option', { name: 'All' })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters }), ); }); diff --git a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 80c4a3a3a2..5135cd4ae6 100644 --- a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -15,19 +15,32 @@ */ import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import { searchApiRef } from '../../api'; import { SearchContextProvider, useSearch } from '../../context'; import { useDefaultFilterValue, useAsyncFilterValues } from './hooks'; +import { configApiRef } from '@backstage/core-plugin-api'; jest.useFakeTimers(); describe('SearchFilter.hooks', () => { describe('useDefaultFilterValue', () => { - const query = jest.fn().mockResolvedValue({}); - const mockApis = TestApiRegistry.from([searchApiRef, { query }]); + const configApiMock = new MockConfigApi({ + search: { + query: { + pageLimit: 100, + }, + }, + }); + const searchApiMock = { + query: jest.fn().mockResolvedValue({ results: [] }), + }; + const mockApis = TestApiRegistry.from( + [searchApiRef, searchApiMock], + [configApiRef, configApiMock], + ); const wrapper = ({ children, overrides = {}, diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 65e65d9436..7c86e16e95 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -18,27 +18,47 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { + MockConfigApi, + renderWithEffects, + TestApiProvider, +} from '@backstage/test-utils'; import { searchApiRef } from '../../api'; import { SearchContextProvider } from '../../context'; import { SearchPagination } from './SearchPagination'; - -const query = jest.fn().mockResolvedValue({ - results: [], - nextPageCursor: 'Mg==', - previousPageCursor: 'MA==', -}); +import { configApiRef } from '@backstage/core-plugin-api'; describe('SearchPagination', () => { + const configApiMock = new MockConfigApi({ + search: { + query: { + pagelimit: 10, + }, + }, + }); + + const searchApiMock = { + query: jest.fn().mockResolvedValue({ + results: [], + nextPageCursor: 'Mg==', + previousPageCursor: 'MA==', + }), + }; + beforeEach(() => { jest.clearAllMocks(); }); it('Renders without exploding', async () => { await renderWithEffects( - + @@ -54,7 +74,12 @@ describe('SearchPagination', () => { it('Define default page limit options', async () => { await renderWithEffects( - + @@ -74,7 +99,12 @@ describe('SearchPagination', () => { it('Accept custom page limit label', async () => { const label = 'Page limit:'; await renderWithEffects( - + @@ -86,7 +116,12 @@ describe('SearchPagination', () => { it('Show the total in text', async () => { await renderWithEffects( - + @@ -98,7 +133,12 @@ describe('SearchPagination', () => { it('Accept custom page limit text', async () => { await renderWithEffects( - + `${from}-${to} of more than ${to}`} @@ -112,7 +152,12 @@ describe('SearchPagination', () => { it('Accept custom page limit options', async () => { await renderWithEffects( - + @@ -131,7 +176,12 @@ describe('SearchPagination', () => { it('Set page limit in the context', async () => { await renderWithEffects( - + @@ -142,7 +192,7 @@ describe('SearchPagination', () => { await userEvent.click(screen.getByText('10')); - expect(query).toHaveBeenCalledWith( + expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ pageLimit: 10, }), @@ -158,7 +208,12 @@ describe('SearchPagination', () => { }; await renderWithEffects( - + @@ -169,7 +224,7 @@ describe('SearchPagination', () => { expect(screen.getByText('51-75')).toBeInTheDocument(); - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ pageCursor: 'Mg==', // page: 2 }), @@ -179,7 +234,7 @@ describe('SearchPagination', () => { expect(screen.getByText('26-50')).toBeInTheDocument(); - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ pageCursor: 'MQ==', // page: 1 }), @@ -195,7 +250,12 @@ describe('SearchPagination', () => { }; await renderWithEffects( - + @@ -205,7 +265,7 @@ describe('SearchPagination', () => { await userEvent.click(screen.getByText('25')); // first click to open the options await userEvent.click(screen.getByText('10')); // second click to select the option - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ pageCursor: undefined, pageLimit: 10, diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 263dab4bfa..df71a848b0 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -27,20 +27,23 @@ import { import { searchApiRef } from '../api'; describe('SearchContext', () => { - const query = jest.fn(); + const searchApiMock = { query: jest.fn() }; - const wrapper = ({ children, initialState, config = {} }: any) => ( - - - {children} - - - ); + const wrapper = ({ children, initialState, config = {} }: any) => { + const configApiMock = new MockConfigApi(config); + return ( + + + {children} + + + ); + }; const initialState = { term: '', @@ -49,7 +52,7 @@ describe('SearchContext', () => { }; beforeEach(() => { - query.mockResolvedValue({}); + searchApiMock.query.mockResolvedValue({}); }); afterAll(() => { @@ -276,7 +279,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ term, types: ['*'], filters: {}, @@ -301,7 +304,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ types, term: '', filters: {}, @@ -326,7 +329,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ filters, term: '', types: ['*'], @@ -351,7 +354,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ pageLimit, term: '', types: ['*'], @@ -377,7 +380,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ pageCursor, term: '', types: ['*'], @@ -386,7 +389,7 @@ describe('SearchContext', () => { }); it('provides function for fetch the next page', async () => { - query.mockResolvedValue({ + searchApiMock.query.mockResolvedValue({ results: [], nextPageCursor: 'NEXT', }); @@ -409,7 +412,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ term: '', types: ['*'], filters: {}, @@ -418,7 +421,7 @@ describe('SearchContext', () => { }); it('provides function for fetch the previous page', async () => { - query.mockResolvedValue({ + searchApiMock.query.mockResolvedValue({ results: [], previousPageCursor: 'PREVIOUS', }); @@ -441,7 +444,7 @@ describe('SearchContext', () => { await waitForNextUpdate(); - expect(query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenLastCalledWith({ term: '', types: ['*'], filters: {}, diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 33c2df9ab8..10be8ba6f8 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -36,16 +36,17 @@ jest.mock('react-router-dom', () => ({ })); describe('SearchModal', () => { - const query = jest.fn().mockResolvedValue({ results: [] }); + const configApiMock = new ConfigReader({ app: { title: 'Mock app' } }); + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; const apiRegistry = TestApiRegistry.from( - [configApiRef, new ConfigReader({ app: { title: 'Mock app' } })], - [searchApiRef, { query }], + [configApiRef, configApiMock], + [searchApiRef, searchApiMock], ); beforeEach(() => { - query.mockClear(); navigate.mockClear(); + searchApiMock.query.mockClear(); }); const toggleModal = jest.fn(); @@ -63,7 +64,7 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(query).toHaveBeenCalledTimes(1); + expect(searchApiMock.query).toHaveBeenCalledTimes(1); }); it('Should use parent search context if defined', async () => { @@ -88,7 +89,7 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(query).toHaveBeenCalledWith(initialState); + expect(searchApiMock.query).toHaveBeenCalledWith(initialState); }); it('Should create a local search context if a parent is not defined', async () => { @@ -104,7 +105,7 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(query).toHaveBeenCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: '', filters: {}, types: [], @@ -141,7 +142,7 @@ describe('SearchModal', () => { }, ); - expect(query).toHaveBeenCalledTimes(1); + expect(searchApiMock.query).toHaveBeenCalledTimes(1); await userEvent.keyboard('{Escape}'); expect(toggleModal).toHaveBeenCalledTimes(1); }); @@ -198,7 +199,7 @@ describe('SearchModal', () => { }, ); - expect(query).toHaveBeenCalledWith( + expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ term: 'term' }), ); @@ -230,7 +231,7 @@ describe('SearchModal', () => { }, ); - expect(query).toHaveBeenCalledWith( + expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ term: 'term' }), ); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index e0acee025a..556a9550a0 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { TestApiProvider } from '@backstage/test-utils'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { act, render, waitFor } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -23,6 +23,7 @@ import { SearchContextProvider, } from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; +import { configApiRef } from '@backstage/core-plugin-api'; const setTypesMock = jest.fn(); const setPageCursorMock = jest.fn(); @@ -40,7 +41,15 @@ jest.mock('@backstage/plugin-search-react', () => ({ })); describe('SearchType.Accordion', () => { - const query = jest.fn(); + const configApiMock = new MockConfigApi({ + search: { + query: { + pagelimit: 10, + }, + }, + }); + + const searchApiMock = { query: jest.fn() }; const expectedLabel = 'Expected Label'; const expectedType = { @@ -50,12 +59,20 @@ describe('SearchType.Accordion', () => { }; beforeEach(() => { - query.mockResolvedValue({ results: [], numberOfResults: 1234 }); + searchApiMock.query.mockResolvedValue({ + results: [], + numberOfResults: 1234, + }); }); const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( - + {children} ); @@ -146,13 +163,13 @@ describe('SearchType.Accordion', () => { , ); - expect(query).toHaveBeenCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: 'abc', types: [], filters: { foo: 'bar' }, pageLimit: 0, }); - expect(query).toHaveBeenCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: 'abc', types: [expectedType.value], filters: {}, diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index 16df3a8f65..c0d8549253 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { TestApiProvider } from '@backstage/test-utils'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -23,6 +23,7 @@ import { searchApiRef, } from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; +import { configApiRef } from '@backstage/core-plugin-api'; const setTypesMock = jest.fn(); const setPageCursorMock = jest.fn(); @@ -38,7 +39,14 @@ jest.mock('@backstage/plugin-search-react', () => ({ })); describe('SearchType.Tabs', () => { - const query = jest.fn().mockResolvedValue({}); + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; + const configApiMock = new MockConfigApi({ + search: { + query: { + pageLimit: 100, + }, + }, + }); const expectedType = { value: 'expected-type', @@ -47,7 +55,12 @@ describe('SearchType.Tabs', () => { const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( - + {children} ); diff --git a/plugins/search/src/components/SearchType/SearchType.test.tsx b/plugins/search/src/components/SearchType/SearchType.test.tsx index a80782921f..353b45c0fb 100644 --- a/plugins/search/src/components/SearchType/SearchType.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.test.tsx @@ -14,17 +14,16 @@ * limitations under the License. */ -import { useApi } from '@backstage/core-plugin-api'; +import { configApiRef } from '@backstage/core-plugin-api'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { SearchContextProvider } from '@backstage/plugin-search-react'; +import { + SearchContextProvider, + searchApiRef, +} from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), - useApi: jest.fn().mockReturnValue({}), -})); +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; describe('SearchType', () => { const initialState = { @@ -37,8 +36,15 @@ describe('SearchType', () => { const values = ['value1', 'value2']; const typeValues = ['preselected']; - const query = jest.fn().mockResolvedValue({}); - (useApi as jest.Mock).mockReturnValue({ query: query }); + const configApiMock = new MockConfigApi({ + search: { + query: { + pagelimit: 10, + }, + }, + }); + + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; afterAll(() => { jest.resetAllMocks(); @@ -47,9 +53,16 @@ describe('SearchType', () => { describe('Type Filter', () => { it('Renders field name and values when provided as props', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -72,14 +85,21 @@ describe('SearchType', () => { it('Renders correctly based on type filter state', async () => { render( - - - , + + + + , ); await waitFor(() => { @@ -103,9 +123,16 @@ describe('SearchType', () => { it('Renders correctly based on type filter defaultValue', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -129,9 +156,16 @@ describe('SearchType', () => { it('Selecting a value sets type filter state', async () => { render( - - - , + + + + + , ); await waitFor(() => { @@ -149,7 +183,7 @@ describe('SearchType', () => { await userEvent.click(screen.getByRole('option', { name: values[0] })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ types: [values[0]], }), @@ -165,14 +199,21 @@ describe('SearchType', () => { it('Selecting none defaults to empty state', async () => { render( - - - , + + + + , ); await waitFor(() => { @@ -190,7 +231,7 @@ describe('SearchType', () => { await userEvent.click(screen.getByRole('option', { name: values[0] })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith( + expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ types: [...typeValues, values[0]], }), @@ -206,7 +247,9 @@ describe('SearchType', () => { await userEvent.click(screen.getByRole('option', { name: values[0] })); await waitFor(() => { - expect(query).toHaveBeenLastCalledWith(expect.objectContaining([])); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + expect.objectContaining([]), + ); }); }); }); From 4c559f8791bff30466da1e8d2500988fa2b1e879 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 22 Aug 2023 14:27:59 +0200 Subject: [PATCH 14/14] fix(search-backend): add config schema back in package json Signed-off-by: Camila Belo --- plugins/search-backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index d46197735f..ba36bea2a9 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -72,5 +72,6 @@ "files": [ "dist", "config.d.ts" - ] + ], + "configSchema": "config.d.ts" }