diff --git a/web/src/layout/navigation/Searchbar.test.tsx b/web/src/layout/navigation/Searchbar.test.tsx
new file mode 100644
index 00000000..7cc12e61
--- /dev/null
+++ b/web/src/layout/navigation/Searchbar.test.tsx
@@ -0,0 +1,367 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { mocked } from 'jest-mock';
+import ReactRouter, { BrowserRouter as Router } from 'react-router-dom';
+
+import API from '../../api';
+import { Project } from '../../types';
+import prepareQueryString from '../../utils/prepareQueryString';
+import Searchbar from './Searchbar';
+jest.mock('../../api');
+
+const mockUseNavigate = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ ...(jest.requireActual('react-router-dom') as any),
+ useNavigate: () => mockUseNavigate,
+ useSearchParams: jest.fn(),
+}));
+
+interface SearchResults {
+ items: Project[];
+ 'Pagination-Total-Count': string;
+}
+
+const getMockSearch = (fixtureId: string): SearchResults => {
+ return require(`./__fixtures__/Searchbar/${fixtureId}.json`) as SearchResults;
+};
+
+const mockSetScrollPosition = jest.fn();
+
+const defaultProps = {
+ setScrollPosition: mockSetScrollPosition,
+};
+
+describe('Searchbar', () => {
+ beforeEach(() => {
+ jest.spyOn(ReactRouter, 'useSearchParams').mockImplementation(() => [new URLSearchParams(''), jest.fn()]);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('creates snapshot', () => {
+ const { asFragment } = render(
+
+
+
+ );
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('renders proper content', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByPlaceholderText('Search projects')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Clear search' })).toBeNull();
+ expect(screen.getByRole('button', { name: 'Search text' })).toBeInTheDocument();
+ });
+
+ it('renders with text', () => {
+ jest.spyOn(ReactRouter, 'useSearchParams').mockImplementation(() => [new URLSearchParams('?text=test'), jest.fn()]);
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
+ expect(screen.getByRole('textbox')).toHaveValue('test');
+ expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Search text' })).toBeInTheDocument();
+ });
+
+ describe('clear btn', () => {
+ it('clear input when text has changed but not search click', () => {
+ jest
+ .spyOn(ReactRouter, 'useSearchParams')
+ .mockImplementation(() => [new URLSearchParams('?text=test'), jest.fn()]);
+
+ render(
+
+
+
+ );
+
+ const clearBtn = screen.getByRole('button', { name: 'Clear search' });
+ const input = screen.getByRole('textbox');
+
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveValue('test');
+ userEvent.type(input, 'ing');
+ userEvent.click(clearBtn);
+ expect(input).toHaveValue('');
+ });
+
+ it('clear input', () => {
+ jest
+ .spyOn(ReactRouter, 'useSearchParams')
+ .mockImplementation(() => [new URLSearchParams('?text=test'), jest.fn()]);
+
+ render(
+
+
+
+ );
+
+ const clearBtn = screen.getByRole('button', { name: 'Clear search' });
+ const input = screen.getByRole('textbox');
+
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveValue('test');
+ userEvent.type(input, 'ing');
+ userEvent.click(clearBtn);
+ expect(input).toHaveValue('');
+ });
+ });
+
+ it('updates value on change input', async () => {
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox');
+
+ userEvent.type(input, 'testing');
+
+ expect(input).toHaveValue('testing');
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('search projects', () => {
+ it('display search results', async () => {
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox') as HTMLInputElement;
+
+ input.focus();
+
+ userEvent.type(input, 'testing');
+ expect(input.value).toBe('testing');
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ expect(API.searchProjects).toHaveBeenCalledWith({
+ limit: 5,
+ offset: 0,
+ sort_by: 'name',
+ sort_direction: 'asc',
+ text: 'testing',
+ });
+ });
+
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ expect(screen.getAllByRole('option')).toHaveLength(8);
+ });
+
+ it("doesn't display results when input is not focused", async () => {
+ const useSearchParamsSpy = jest.spyOn(ReactRouter, 'useSearchParams');
+ useSearchParamsSpy.mockImplementation(() => [new URLSearchParams('?text=test'), jest.fn()]);
+
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByDisplayValue('test');
+
+ input.focus();
+ userEvent.type(input, 'ing');
+ input.blur();
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ input.blur();
+ });
+
+ expect(screen.queryByRole('listbox')).toBeNull();
+ });
+
+ it('loads project detail from search dropdown', async () => {
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox') as HTMLInputElement;
+
+ input.focus();
+
+ userEvent.type(input, 'testing');
+ expect(input.value).toBe('testing');
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ expect(API.searchProjects).toHaveBeenCalledWith({
+ limit: 5,
+ offset: 0,
+ sort_by: 'name',
+ sort_direction: 'asc',
+ text: 'testing',
+ });
+ });
+
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ const items = screen.getAllByRole('option');
+ userEvent.click(items[1]);
+
+ expect(mockUseNavigate).toHaveBeenCalledTimes(1);
+ expect(mockUseNavigate).toHaveBeenCalledWith('/projects/backstage/backstage');
+ });
+
+ it('loads new search from search dropdown', async () => {
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox') as HTMLInputElement;
+
+ input.focus();
+
+ userEvent.type(input, 'testing');
+ expect(input.value).toBe('testing');
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ expect(API.searchProjects).toHaveBeenCalledWith({
+ limit: 5,
+ offset: 0,
+ sort_by: 'name',
+ sort_direction: 'asc',
+ text: 'testing',
+ });
+ });
+
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ const allResults = screen.getByRole('option', { name: 'See all results' });
+ userEvent.click(allResults);
+
+ expect(mockUseNavigate).toHaveBeenCalledTimes(1);
+ expect(mockUseNavigate).toHaveBeenCalledWith({ pathname: '/search', search: '?text=testing&page=1' });
+ });
+
+ it('uses arrow for seleting one item and loads detail to click enter', async () => {
+ const mockSearch = getMockSearch('1');
+ mocked(API).searchProjects.mockResolvedValue(mockSearch);
+
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox') as HTMLInputElement;
+
+ input.focus();
+
+ userEvent.type(input, 'testing');
+ expect(input.value).toBe('testing');
+
+ await waitFor(() => {
+ expect(API.searchProjects).toHaveBeenCalledTimes(1);
+ expect(API.searchProjects).toHaveBeenCalledWith({
+ limit: 5,
+ offset: 0,
+ sort_by: 'name',
+ sort_direction: 'asc',
+ text: 'testing',
+ });
+ });
+
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ const options = screen.getAllByRole('option');
+ expect(options[0]).not.toHaveClass('activeDropdownItem');
+
+ userEvent.keyboard('{arrowdown}');
+
+ await waitFor(() => {
+ expect(options[0]).toHaveClass('activeDropdownItem');
+ });
+
+ userEvent.keyboard('{arrowdown}{arrowdown}');
+
+ expect(options[0]).not.toHaveClass('activeDropdownItem');
+ expect(options[2]).toHaveClass('activeDropdownItem');
+
+ userEvent.keyboard('{enter}');
+
+ expect(mockUseNavigate).toHaveBeenCalledTimes(1);
+ expect(mockUseNavigate).toHaveBeenCalledWith('/projects/keptn/keptn');
+ });
+ });
+
+ describe('Navigate', () => {
+ it('calls on Enter key press', () => {
+ render(
+
+
+
+ );
+
+ const input = screen.getByRole('textbox');
+ userEvent.type(input, 'testing{enter}');
+ expect(input).not.toBe(document.activeElement);
+ expect(mockUseNavigate).toHaveBeenCalledTimes(1);
+ expect(mockUseNavigate).toHaveBeenCalledWith({
+ pathname: '/search',
+ search: prepareQueryString({
+ text: 'testing',
+ pageNumber: 1,
+ }),
+ });
+ });
+
+ it('calls navigate on Enter key press when text is empty with undefined text', () => {
+ render(
+
+
+
+ );
+
+ const input = screen.getByPlaceholderText('Search projects');
+ userEvent.type(input, '{enter}');
+ expect(mockUseNavigate).toHaveBeenCalledTimes(1);
+ expect(mockUseNavigate).toHaveBeenCalledWith({
+ pathname: '/search',
+ search: prepareQueryString({
+ text: undefined,
+ pageNumber: 1,
+ }),
+ });
+ });
+ });
+});
diff --git a/web/src/layout/navigation/Searchbar.tsx b/web/src/layout/navigation/Searchbar.tsx
index 42310926..31ea2cab 100644
--- a/web/src/layout/navigation/Searchbar.tsx
+++ b/web/src/layout/navigation/Searchbar.tsx
@@ -186,7 +186,7 @@ const Searchbar = (props: Props) => {
const text = searchParams.get('text');
setValue(text || '');
setCurrentSearch(text);
- }, [searchParams]);
+ }, []); /* eslint-disable-line react-hooks/exhaustive-deps */
useEffect(() => {
// Don't display search options for mobile devices
diff --git a/web/src/layout/navigation/__snapshots__/Searchbar.test.tsx.snap b/web/src/layout/navigation/__snapshots__/Searchbar.test.tsx.snap
new file mode 100644
index 00000000..8f21148a
--- /dev/null
+++ b/web/src/layout/navigation/__snapshots__/Searchbar.test.tsx.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Searchbar creates snapshot 1`] = `
+
+
+
+`;
diff --git a/web/src/layout/notFound/__snapshots__/index.test.tsx.snap b/web/src/layout/notFound/__snapshots__/index.test.tsx.snap
index 918fd9c9..7a12f84d 100644
--- a/web/src/layout/notFound/__snapshots__/index.test.tsx.snap
+++ b/web/src/layout/notFound/__snapshots__/index.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`NotFount creates snapshot 1`] = `
+exports[`NotFound creates snapshot 1`] = `
{
+describe('NotFound', () => {
afterEach(() => {
jest.resetAllMocks();
});
diff --git a/web/src/layout/search/filters/Section.test.tsx b/web/src/layout/search/filters/Section.test.tsx
new file mode 100644
index 00000000..3a4c3a82
--- /dev/null
+++ b/web/src/layout/search/filters/Section.test.tsx
@@ -0,0 +1,78 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { FilterKind, Maturity } from '../../../types';
+import Section from './Section';
+
+const mockOnChange = jest.fn();
+
+const defaultProps = {
+ section: {
+ name: FilterKind.Maturity,
+ title: 'Maturity level',
+ filters: [
+ { name: Maturity.Graduated, label: 'Graduated' },
+ { name: Maturity.Incubating, label: 'Incubating' },
+ { name: Maturity.Sandbox, label: 'Sandbox' },
+ ],
+ },
+ activeFilters: [],
+ onChange: mockOnChange,
+ device: 'test',
+};
+
+describe('Section', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('creates snapshot', () => {
+ const { asFragment } = render();
+
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ describe('Render', () => {
+ it('renders Section', () => {
+ render();
+
+ expect(screen.getByText('Maturity level')).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Graduated' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeInTheDocument();
+ });
+
+ it('renders Section with selected options', () => {
+ render();
+
+ expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeChecked();
+ });
+
+ it('calls onChange to click filter', () => {
+ render();
+
+ const check = screen.getByRole('checkbox', { name: 'Incubating' });
+
+ expect(check).not.toBeChecked();
+
+ userEvent.click(check);
+
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
+ expect(mockOnChange).toHaveBeenCalledWith('maturity', '1', true);
+ });
+
+ it('calls onChange to click selected filter', () => {
+ render();
+
+ const check = screen.getByRole('checkbox', { name: 'Graduated' });
+
+ expect(check).toBeChecked();
+
+ userEvent.click(check);
+
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
+ expect(mockOnChange).toHaveBeenCalledWith('maturity', '0', false);
+ });
+ });
+});
diff --git a/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap b/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap
new file mode 100644
index 00000000..9edd7ef7
--- /dev/null
+++ b/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Section creates snapshot 1`] = `
+
+
+
+ Maturity level
+
+
+
+
+`;
diff --git a/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap b/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000..e842d62a
--- /dev/null
+++ b/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,522 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Filters creates snapshot 1`] = `
+
+
+
+
+ Maturity level
+
+
+
+
+
+ Rating
+
+
+
+
+
+ Category
+
+
+
+
+`;
diff --git a/web/src/layout/search/filters/index.test.tsx b/web/src/layout/search/filters/index.test.tsx
new file mode 100644
index 00000000..3855f1e9
--- /dev/null
+++ b/web/src/layout/search/filters/index.test.tsx
@@ -0,0 +1,74 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import Filters from './index';
+
+const mockOnChange = jest.fn();
+
+const defaultProps = {
+ visibleTitle: true,
+ activeFilters: {},
+ onChange: mockOnChange,
+ device: 'test',
+};
+
+describe('Filters', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('creates snapshot', () => {
+ const { asFragment } = render();
+
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ describe('Render', () => {
+ it('renders Filters', () => {
+ render();
+
+ expect(screen.getByText('Filters')).toBeInTheDocument();
+
+ expect(screen.getByText('Maturity level')).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Graduated' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeInTheDocument();
+
+ expect(screen.getByText('Rating')).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'C [25-49]' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'D [0-24]' })).toBeInTheDocument();
+
+ expect(screen.getByText('Category')).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'App definition' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Observability' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Orchestration' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Platform' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Provisioning' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Runtime' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'Serverless' })).toBeInTheDocument();
+ });
+
+ it('renders Filters with selected options', () => {
+ render();
+
+ expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeChecked();
+ });
+
+ it('calls onChange to click filter', () => {
+ render();
+
+ const check = screen.getByRole('checkbox', { name: 'App definition' });
+
+ expect(check).not.toBeChecked();
+
+ userEvent.click(check);
+
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
+ expect(mockOnChange).toHaveBeenCalledWith('category', '0', true);
+ });
+ });
+});