Skip to content

Commit

Permalink
fix(global-header): added unit tests for searchBar
Browse files Browse the repository at this point in the history
Signed-off-by: Yi Cai <[email protected]>
  • Loading branch information
ciiay committed Jan 8, 2025
1 parent ff672f2 commit 82882eb
Show file tree
Hide file tree
Showing 13 changed files with 687 additions and 76 deletions.
3 changes: 3 additions & 0 deletions workspaces/global-header/plugins/global-header/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"lint:fix": "backstage-cli package lint --fix",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
Expand All @@ -40,6 +41,7 @@
"@backstage/plugin-search-backend-module-catalog": "^0.2.6",
"@backstage/plugin-search-backend-module-pg": "^0.5.39",
"@backstage/plugin-search-backend-module-techdocs": "^0.3.4",
"@backstage/plugin-search-common": "^1.2.16",
"@backstage/plugin-search-react": "1.8.4",
"@backstage/theme": "^0.6.2",
"@mui/icons-material": "5.16.13",
Expand All @@ -57,6 +59,7 @@
"@backstage/cli": "^0.29.0",
"@backstage/core-app-api": "^1.15.2",
"@backstage/dev-utils": "^1.1.4",
"@backstage/plugin-search-common": "^1.2.16",
"@backstage/test-utils": "^1.7.2",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { SearchBar } from './SearchBar';
import {
searchApiRef,
SearchContextProvider,
} from '@backstage/plugin-search-react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { MemoryRouter, useNavigate } from 'react-router-dom';
import { configApiRef } from '@backstage/core-plugin-api';

const createInitialState = ({
term = '',
filters = {},
types = ['*'],
pageCursor = '',
} = {}) => ({
term,
filters,
types,
pageCursor,
});

const mockSearchApi = {
query: jest.fn().mockResolvedValue({
results: [
{
type: 'software-catalog',
document: {
title: 'Example Result',
location: '/catalog/default/component/example',
},
},
],
}),
};

const mockConfig = mockApis.config({
data: {
app: {
url: 'https://example.com',
},
},
});

jest.mock('../../utils/stringUtils', () => ({
...jest.requireActual('../../utils/stringUtils'),
highlightMatch: jest.fn(text => text),
}));

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}));

describe('SearchBar', () => {
const setup = (term = '') => {
const setSearchTerm = jest.fn();
const navigate = jest.fn();
jest.mocked(useNavigate).mockReturnValue(navigate);

render(
<MemoryRouter initialEntries={['/']}>
<TestApiProvider
apis={[
[searchApiRef, mockSearchApi],
[configApiRef, mockConfig],
]}
>
<SearchContextProvider initialState={createInitialState({ term })}>
<SearchBar query={{ term }} setSearchTerm={setSearchTerm} />
</SearchContextProvider>
</TestApiProvider>
</MemoryRouter>,
);
return { setSearchTerm, navigate };
};

it('renders the search input', () => {
setup();
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
});

it('calls setSearchTerm on input change', () => {
const { setSearchTerm } = setup();
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'test' },
});
expect(setSearchTerm).toHaveBeenCalledWith('test');
});

it('displays "No results found" when there are no results', async () => {
mockSearchApi.query.mockResolvedValueOnce({ results: [] });
setup('test');
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'test' },
});
await waitFor(() => {
expect(screen.getByText('No results found')).toBeInTheDocument();
});
});

it('displays search results', async () => {
setup('example');
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'example' },
});
await waitFor(() => {
expect(screen.getByText('Example Result')).toBeInTheDocument();
});
});

it('navigates to the search page on Enter key press', async () => {
const { navigate } = setup('example');
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'example' },
});
await waitFor(() => {
expect(screen.getByText('Example Result')).toBeInTheDocument();
});

fireEvent.keyDown(screen.getByPlaceholderText('Search...'), {
key: 'Enter',
code: 'Enter',
});
await waitFor(() => {
expect(navigate).toHaveBeenCalledWith('/search?query=example');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,19 +19,11 @@ import {
SearchResultState,
SearchResultProps,
} from '@backstage/plugin-search-react';
import Typography from '@mui/material/Typography';
import { Link } from '@backstage/core-components';
import ListItem from '@mui/material/ListItem';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import SearchIcon from '@mui/icons-material/Search';
import { createSearchLink, highlightMatch } from '../../utils/stringUtils';
import styles from './SearchBar.module.css';
import { createSearchLink } from '../../utils/stringUtils';
import { useNavigate } from 'react-router-dom';
import { SearchInput } from './SearchInput';
import { SearchOption } from './SearchOption';

interface SearchBarProps {
query: SearchResultProps['query'];
Expand Down Expand Up @@ -75,72 +67,23 @@ export const SearchBar = (props: SearchBarProps) => {
}
}}
renderInput={params => (
<TextField
{...params}
placeholder="Search..."
variant="standard"
<SearchInput
params={params}
error={!!error}
helperText={error ? 'Error fetching results' : ''}
InputProps={{
...params.InputProps,
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ color: '#fff' }} />
</InputAdornment>
),
}}
sx={{
pt: '6px',
input: { color: '#fff' },
button: { color: '#fff' },
}}
/>
)}
renderOption={(renderProps, option, { index }) => {
if (option === query?.term && index === options.length - 1) {
return (
<Box key="all-results" id="all-results">
<Divider sx={{ my: 0.5 }} />
<Link to={searchLink} underline="none">
<ListItem
{...renderProps}
sx={{ my: 0 }}
className={styles.allResultsOption}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ flexGrow: 1, mr: 1 }}>
All results
</Typography>
<ArrowForwardIcon fontSize="small" />
</Box>
</ListItem>
</Link>
</Box>
);
}

const result = results.find(r => r.document.title === option);
return (
<Link
to={result?.document.location ?? '#'}
underline="none"
key={option}
>
<ListItem {...renderProps} sx={{ cursor: 'pointer', py: 2 }}>
<Box sx={{ display: 'flex', width: '100%' }}>
<Typography
sx={{ color: 'text.primary', py: 0.5, flexGrow: 1 }}
>
{option === 'No results found'
? option
: highlightMatch(option, query?.term ?? '')}
</Typography>
</Box>
</ListItem>
</Link>
);
}}
renderOption={(renderProps, option, { index }) => (
<SearchOption
option={option}
index={index}
options={options}
query={query}
results={results}
renderProps={renderProps}
searchLink={searchLink}
/>
)}
ListboxProps={{
style: { maxHeight: 600 },
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { SearchComponent } from './SearchComponent';
import {
searchApiRef,
SearchContextProvider,
} from '@backstage/plugin-search-react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { configApiRef } from '@backstage/core-plugin-api';

jest.mock('./SearchBar', () => ({
SearchBar: ({
query,
setSearchTerm,
}: {
query: { term: string };
setSearchTerm: (term: string) => void;
}) => (
<input
data-testid="search-bar"
value={query.term}
onChange={e => setSearchTerm(e.target.value)}
/>
),
}));

const mockSearchApi = {
query: jest.fn().mockResolvedValue({
results: [
{
type: 'software-catalog',
document: {
title: 'Example Result',
location: '/catalog/default/component/example',
},
},
],
}),
};

const mockConfig = mockApis.config({
data: { app: { baseUrl: 'https://example.com' } },
});

describe('SearchComponent', () => {
it('should render the SearchBar with initial state', () => {
render(
<TestApiProvider
apis={[
[searchApiRef, mockSearchApi],
[configApiRef, mockConfig],
]}
>
<SearchContextProvider>
<SearchComponent />
</SearchContextProvider>
</TestApiProvider>,
);

const searchBar = screen.getByTestId('search-bar');
expect(searchBar).toBeInTheDocument();
expect(searchBar).toHaveValue('');
});

it('should update the search term when typing in the SearchBar', () => {
render(
<TestApiProvider
apis={[
[searchApiRef, mockSearchApi],
[configApiRef, mockConfig],
]}
>
<SearchContextProvider>
<SearchComponent />
</SearchContextProvider>
</TestApiProvider>,
);

const searchBar = screen.getByTestId('search-bar');
fireEvent.change(searchBar, { target: { value: 'example' } });

expect(searchBar).toHaveValue('example');
});
});
Loading

0 comments on commit 82882eb

Please sign in to comment.