diff --git a/.changeset/silent-eyes-lie.md b/.changeset/silent-eyes-lie.md new file mode 100644 index 0000000000..e9da3d2685 --- /dev/null +++ b/.changeset/silent-eyes-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': minor +--- + +Added the icons option to the renderInTestApp function's TestAppOptions to be forwarded to the icons option of `createApp`. diff --git a/.changeset/sixty-rabbits-cheat.md b/.changeset/sixty-rabbits-cheat.md new file mode 100644 index 0000000000..0161e272ed --- /dev/null +++ b/.changeset/sixty-rabbits-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': minor +--- + +Make use of the `useApp` hook to retrieve the specified search icon in the SearchBar diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index cc1c645387..ae54c588ba 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -8,6 +8,7 @@ import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AppComponents } from '@backstage/core-plugin-api'; +import { AppIcons } from '@backstage/core-app-api'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; @@ -21,6 +22,7 @@ import { EvaluatePermissionRequest } from '@backstage/plugin-permission-common'; import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; @@ -252,6 +254,9 @@ export type TestAppOptions = { [path: string]: RouteRef | ExternalRouteRef; }; components?: Partial; + icons?: Partial & { + [key in string]: IconComponent; + }; }; // @public diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index f2c1ff592e..3a55389b74 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -23,13 +23,14 @@ import React, { import { MemoryRouter, Route } from 'react-router-dom'; import { themes, UnifiedThemeProvider } from '@backstage/theme'; import MockIcon from '@material-ui/icons/AcUnit'; -import { createSpecializedApp } from '@backstage/core-app-api'; +import { AppIcons, createSpecializedApp } from '@backstage/core-app-api'; import { AppComponents, attachComponentData, BootErrorPageProps, createRouteRef, ExternalRouteRef, + IconComponent, RouteRef, } from '@backstage/core-plugin-api'; import { MatcherFunction, RenderResult } from '@testing-library/react'; @@ -107,6 +108,13 @@ export type TestAppOptions = { * Components to be forwarded to the `components` option of `createApp`. */ components?: Partial; + + /** + * Icons to be forwarded to the `icons` option of `createApp`. + */ + icons?: Partial & { + [key in string]: IconComponent; + }; }; function isExternalRouteRef( @@ -145,7 +153,10 @@ export function createTestAppWrapper( ), ...options.components, }, - icons: mockIcons, + icons: { + ...mockIcons, + ...options.icons, + }, plugins: [], themes: [ { diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx index eb113cb5fd..57f0c01f39 100644 --- a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx @@ -19,7 +19,7 @@ import React, { ComponentType, PropsWithChildren } from 'react'; import Grid from '@material-ui/core/Grid'; import LabelIcon from '@material-ui/icons/Label'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; @@ -31,17 +31,18 @@ export default { title: 'Plugins/Search/SearchAutocomplete', component: SearchAutocomplete, decorators: [ - (Story: ComponentType>) => ( - - - - - + (Story: ComponentType>) => + wrapInTestApp( + + + + + + - - - - ), + + , + ), ], }; diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx index d9da187541..cc1e6c486c 100644 --- a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx @@ -22,7 +22,7 @@ import LabelIcon from '@material-ui/icons/Label'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; -import { TestApiProvider, renderWithEffects } from '@backstage/test-utils'; +import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; import { SearchAutocomplete } from './SearchAutocomplete'; @@ -44,7 +44,7 @@ describe('SearchAutocomplete', () => { }); it('Renders without exploding', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Show all options by default when focused', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Updates context with the initial value', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Updates context when value is cleared', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Updates context when an option is select', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Shows a circular progress when loading options', async () => { - await renderWithEffects( + await renderInTestApp( { }); it('Uses the default search autocomplete option component', async () => { - await renderWithEffects( + await renderInTestApp( ({ component: (await import('./SearchBar')).SearchBar }), + ], decorators: [ - (Story: ComponentType>) => ( - - - - - + (Story: ComponentType>) => + wrapInTestApp( + + + + + + - - - - ), + + , + ), ], }; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 6fcb3ef987..3f667a4ad6 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -22,7 +22,7 @@ import { ConfigReader } from '@backstage/core-app-api'; import { MockAnalyticsApi, TestApiProvider, - renderWithEffects, + renderInTestApp, } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; import { SearchContextProvider } from '../../context'; @@ -61,7 +61,7 @@ describe('SearchBar', () => { }); it('Renders without exploding', async () => { - await renderWithEffects( + await renderInTestApp( { it('Renders with custom label', async () => { const label = 'label'; - await renderWithEffects( + await renderInTestApp( { it('Renders with custom placeholder', async () => { const placeholder = 'placeholder'; - await renderWithEffects( + await renderInTestApp( { it('Renders based on initial search', async () => { const term = 'term'; - await renderWithEffects( + await renderInTestApp( { it('Updates term state when text is entered', async () => { jest.useFakeTimers(); - await renderWithEffects( + await renderInTestApp( { it('Clear button clears term state', async () => { const term = 'term'; - await renderWithEffects( + await renderInTestApp( { it('Should not show clear button', async () => { const term = 'term'; - await renderWithEffects( + await renderInTestApp( { const debounceTime = 100; - await renderWithEffects( + await renderInTestApp( { it('Does not capture analytics event if not enabled in app', async () => { const analyticsApiMock = new MockAnalyticsApi(); - await renderWithEffects( + await renderInTestApp( { expect(analyticsApiMock.getEvents()).toHaveLength(0); }); + + it('Renders custom search icon', async () => { + const CustomSearchIcon = () => ( + + + + ); + + await renderInTestApp( + + + + + , + { + icons: { + search: CustomSearchIcon, + }, + }, + ); + + const queryButton = screen.getByLabelText('Query'); + + expect(queryButton).toBeInTheDocument(); + expect(queryButton.innerHTML).toContain( + '', + ); + }); }); diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index 254f39e48e..c6976508cc 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -18,13 +18,14 @@ import { AnalyticsContext, configApiRef, useApi, + useApp, } from '@backstage/core-plugin-api'; import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; import { TextFieldProps } from '@material-ui/core/TextField'; -import SearchIcon from '@material-ui/icons/Search'; +import DefaultSearchIcon from '@material-ui/icons/Search'; import React, { ChangeEvent, ComponentType, @@ -146,6 +147,8 @@ export const SearchBarBase: ForwardRefExoticComponent = placeholder ?? `Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`; + const SearchIcon = useApp().getSystemIcon('search') || DefaultSearchIcon; + const startAdornment = (