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`] = ` + +
+
+ Filters +
+
+
+ + Maturity level + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Rating + +
+
+
+ +