diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 4f00b458d39..e501eadde04 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -149,4 +149,8 @@ export const Components = { ValuePicker: { select: (name: string) => `Value picker select ${name}`, }, + Search: { + section: 'Search section', + items: 'Search items', + }, }; diff --git a/public/app/core/services/__mocks__/search_srv.ts b/public/app/core/services/__mocks__/search_srv.ts new file mode 100644 index 00000000000..6c6c60b47f6 --- /dev/null +++ b/public/app/core/services/__mocks__/search_srv.ts @@ -0,0 +1,22 @@ +export const mockSearch = jest.fn(() => { + return Promise.resolve([]); +}); + +export const mockGetDashboardTags = jest.fn(async () => + Promise.resolve([ + { term: 'tag1', count: 2 }, + { term: 'tag2', count: 10 }, + ]) +); + +export const mockGetSortOptions = jest.fn(() => + Promise.resolve({ sortOptions: [{ name: 'test', displayName: 'Test' }] }) +); + +export const SearchSrv = jest.fn().mockImplementation(() => { + return { + search: mockSearch, + getDashboardTags: mockGetDashboardTags, + getSortOptions: mockGetSortOptions, + }; +}); diff --git a/public/app/features/search/components/DashboardSearch.test.tsx b/public/app/features/search/components/DashboardSearch.test.tsx index ea8ab5ac6f8..7319e3c8598 100644 --- a/public/app/features/search/components/DashboardSearch.test.tsx +++ b/public/app/features/search/components/DashboardSearch.test.tsx @@ -1,11 +1,17 @@ import React from 'react'; import { render, fireEvent, screen, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { mockSearch } from './mocks'; +import * as SearchSrv from 'app/core/services/search_srv'; +import * as MockSearchSrv from 'app/core/services/__mocks__/search_srv'; import { DashboardSearch, Props } from './DashboardSearch'; import { searchResults } from '../testData'; import { SearchLayout } from '../types'; +jest.mock('app/core/services/search_srv'); +// Typecast the mock search so the mock import is correctly recognised by TS +// https://stackoverflow.com/a/53222290 +const { mockSearch } = SearchSrv as typeof MockSearchSrv; + beforeEach(() => { jest.useFakeTimers(); mockSearch.mockClear(); @@ -81,7 +87,7 @@ describe('DashboardSearch', () => { setup(); const section = await screen.findAllByLabelText('Search section'); expect(section).toHaveLength(2); - expect(screen.getAllByLabelText('Search items')).toHaveLength(2); + expect(screen.getAllByLabelText('Search items')).toHaveLength(1); }); it('should call search with selected tags', async () => { diff --git a/public/app/features/search/components/SearchItem.test.tsx b/public/app/features/search/components/SearchItem.test.tsx index aee6d6d8a0c..e8b4ce93945 100644 --- a/public/app/features/search/components/SearchItem.test.tsx +++ b/public/app/features/search/components/SearchItem.test.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import { mount } from 'enzyme'; -import { Tag } from '@grafana/ui'; +import { render, screen, fireEvent } from '@testing-library/react'; import { SearchItem, Props } from './SearchItem'; import { DashboardSearchItemType } from '../types'; +beforeEach(() => { + jest.clearAllMocks(); +}); + const data = { id: 1, uid: 'lBdLINUWk', @@ -22,31 +25,41 @@ const setup = (propOverrides?: Partial) => { item: data, onTagSelected: jest.fn(), editable: false, - //@ts-ignore - onToggleAllChecked: jest.fn(), }; Object.assign(props, propOverrides); - const wrapper = mount(); - const instance = wrapper.instance(); - - return { - wrapper, - instance, - }; + render(); }; describe('SearchItem', () => { it('should render the item', () => { - const { wrapper } = setup({}); - expect(wrapper.find({ 'aria-label': 'Dashboard search item Test 1' })).toHaveLength(1); - expect(wrapper.findWhere(comp => comp.type() === 'div' && comp.text() === 'Test 1')).toHaveLength(1); + setup(); + expect(screen.getAllByLabelText('Dashboard search item Test 1')).toHaveLength(1); + expect(screen.getAllByText('Test 1')).toHaveLength(1); + }); + + it('should mark item as checked', () => { + const mockedOnToggleChecked = jest.fn(); + setup({ editable: true, onToggleChecked: mockedOnToggleChecked }); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + fireEvent.click(checkbox); + expect(mockedOnToggleChecked).toHaveBeenCalledTimes(1); + expect(mockedOnToggleChecked).toHaveBeenCalledWith(data); + expect(screen.getByRole('checkbox')).toBeChecked(); }); it("should render item's tags", () => { - // @ts-ignore - const { wrapper } = setup({}); - expect(wrapper.find(Tag)).toHaveLength(2); + setup(); + expect(screen.getAllByText(/tag/i)).toHaveLength(2); + }); + + it('should select the tag on tag click', () => { + const mockOnTagSelected = jest.fn(); + setup({ onTagSelected: mockOnTagSelected }); + fireEvent.click(screen.getByText('Tag1')); + expect(mockOnTagSelected).toHaveBeenCalledTimes(1); + expect(mockOnTagSelected).toHaveBeenCalledWith('Tag1'); }); }); diff --git a/public/app/features/search/components/SearchResults.test.tsx b/public/app/features/search/components/SearchResults.test.tsx index fb649cfe84b..d23d85bfe10 100644 --- a/public/app/features/search/components/SearchResults.test.tsx +++ b/public/app/features/search/components/SearchResults.test.tsx @@ -1,52 +1,56 @@ import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { render, screen, fireEvent } from '@testing-library/react'; import { SearchResults, Props } from './SearchResults'; -import { searchResults } from '../testData'; +import { searchResults, generalFolder } from '../testData'; import { SearchLayout } from '../types'; -const setup = (propOverrides?: Partial, renderMethod = shallow) => { +beforeEach(() => { + jest.clearAllMocks(); +}); + +const setup = (propOverrides?: Partial) => { const props: Props = { - //@ts-ignore results: searchResults, - onSelectionChanged: () => {}, onTagSelected: (name: string) => {}, - onFolderExpanding: () => {}, - onToggleSelection: () => {}, + onToggleSection: () => {}, editable: false, layout: SearchLayout.Folders, }; Object.assign(props, propOverrides); - const wrapper = renderMethod(); - const instance = wrapper.instance(); - - return { - wrapper, - instance, - }; + render(); }; describe('SearchResults', () => { it('should render result items', () => { - const { wrapper } = setup(); - expect(wrapper.find({ 'aria-label': 'Search section' })).toHaveLength(2); + setup(); + expect(screen.getAllByLabelText('Search section')).toHaveLength(2); }); it('should render section items for expanded section', () => { - const { wrapper } = setup(); - expect(wrapper.find({ 'aria-label': 'Search items' }).children()).toHaveLength(2); + setup(); + expect(screen.getAllByLabelText(/collapse folder/i)).toHaveLength(1); + expect(screen.getAllByLabelText('Search items')).toHaveLength(1); + expect(screen.getAllByLabelText(/dashboard search item/i)).toHaveLength(2); }); it('should not render checkboxes for non-editable results', () => { - //@ts-ignore - const { wrapper } = setup({ editable: false }, mount); - expect(wrapper.find({ type: 'checkbox' })).toHaveLength(0); + setup({ editable: false }); + expect(screen.queryAllByRole('checkbox')).toHaveLength(0); }); - it('should render checkboxes for non-editable results', () => { - //@ts-ignore - const { wrapper } = setup({ editable: true }, mount); - expect(wrapper.find({ type: 'checkbox' })).toHaveLength(4); + it('should render checkboxes for editable results', () => { + setup({ editable: true }); + expect(screen.getAllByRole('checkbox')).toHaveLength(4); + }); + + it('should collapse folder item list on header click', () => { + const mockOnToggleSection = jest.fn(); + setup({ onToggleSection: mockOnToggleSection }); + + fireEvent.click(screen.getByLabelText('Collapse folder 0')); + expect(mockOnToggleSection).toHaveBeenCalledTimes(1); + expect(mockOnToggleSection).toHaveBeenCalledWith(generalFolder); }); }); diff --git a/public/app/features/search/components/SearchResults.tsx b/public/app/features/search/components/SearchResults.tsx index ef444ddea8c..e77fce0c0a8 100644 --- a/public/app/features/search/components/SearchResults.tsx +++ b/public/app/features/search/components/SearchResults.tsx @@ -4,6 +4,7 @@ import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme } from '@grafana/data'; import { stylesFactory, useTheme, Spinner } from '@grafana/ui'; +import { selectors } from '@grafana/e2e-selectors'; import { DashboardSection, OnToggleChecked, SearchLayout } from '../types'; import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants'; import { SearchItem } from './SearchItem'; @@ -19,6 +20,8 @@ export interface Props { layout?: string; } +const { section: sectionLabel, items: itemsLabel } = selectors.components.Search; + export const SearchResults: FC = ({ editable, loading, @@ -36,11 +39,15 @@ export const SearchResults: FC = ({
{results.map(section => { return ( -
+
-
- {section.expanded && section.items.map(item => )} -
+ {section.expanded && ( +
+ {section.items.map(item => ( + + ))} +
+ )}
); })} diff --git a/public/app/features/search/components/SearchResultsFilter.test.tsx b/public/app/features/search/components/SearchResultsFilter.test.tsx index f580a8a749b..4d5d1805d83 100644 --- a/public/app/features/search/components/SearchResultsFilter.test.tsx +++ b/public/app/features/search/components/SearchResultsFilter.test.tsx @@ -1,15 +1,29 @@ import React from 'react'; -import { mount, shallow } from 'enzyme'; -import { SearchResultsFilter, Props } from './SearchResultsFilter'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { Props, SearchResultsFilter } from './SearchResultsFilter'; +import { SearchLayout } from '../types'; + +jest.mock('app/core/services/search_srv'); const noop = jest.fn(); -const findBtnByText = (wrapper: any, text: string) => - wrapper.findWhere((c: any) => c.name() === 'Button' && c.text() === text); +beforeEach(() => { + jest.clearAllMocks(); +}); -const setup = (propOverrides?: Partial, renderMethod = shallow) => { +const searchQuery = { + starred: false, + sort: null, + tag: ['tag'], + query: '', + skipRecent: true, + skipStarred: true, + folderIds: [], + layout: SearchLayout.Folders, +}; + +const setup = (propOverrides?: Partial) => { const props: Props = { - //@ts-ignore allChecked: false, canDelete: false, canMove: false, @@ -18,73 +32,57 @@ const setup = (propOverrides?: Partial, renderMethod = shallow) => { onStarredFilterChange: noop, onTagFilterChange: noop, onToggleAllChecked: noop, - //@ts-ignore - query: { starred: false, sort: null, tag: ['tag'] }, + onLayoutChange: noop, + query: searchQuery, onSortChange: noop, editable: true, }; Object.assign(props, propOverrides); - const wrapper = renderMethod(); - const instance = wrapper.instance(); - - return { - wrapper, - instance, - }; + render(); }; describe('SearchResultsFilter', () => { - it('should render "filter by starred" and "filter by tag" filters by default', () => { - const { wrapper } = setup(); - const ActionRow = wrapper.find('ActionRow').shallow(); - expect(ActionRow.find('Checkbox')).toHaveLength(1); - expect(findBtnByText(wrapper, 'Move')).toHaveLength(0); - expect(findBtnByText(wrapper, 'Delete')).toHaveLength(0); + it('should render "filter by starred" and "filter by tag" filters by default', async () => { + setup(); + expect(await screen.findAllByRole('checkbox')).toHaveLength(1); + expect(screen.queryByText('Move')).not.toBeInTheDocument(); + expect(screen.queryByText('Delete')).not.toBeInTheDocument(); }); it('should render Move and Delete buttons when canDelete is true', () => { - const { wrapper } = setup({ canDelete: true }); - expect(wrapper.find('Checkbox')).toHaveLength(1); - expect(findBtnByText(wrapper, 'Move')).toHaveLength(1); - expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1); + setup({ canDelete: true }); + expect(screen.getAllByRole('checkbox')).toHaveLength(1); + expect(screen.queryByText('Move')).toBeInTheDocument(); + expect(screen.queryByText('Delete')).toBeInTheDocument(); }); it('should render Move and Delete buttons when canMove is true', () => { - const { wrapper } = setup({ canMove: true }); - expect(wrapper.find('Checkbox')).toHaveLength(1); - expect(findBtnByText(wrapper, 'Move')).toHaveLength(1); - expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1); + setup({ canMove: true }); + expect(screen.getAllByRole('checkbox')).toHaveLength(1); + expect(screen.queryByText('Move')).toBeInTheDocument(); + expect(screen.queryByText('Delete')).toBeInTheDocument(); }); - it('should be called with proper filter option when "filter by starred" is changed', () => { + it('should call onStarredFilterChange when "filter by starred" is changed', async () => { const mockFilterStarred = jest.fn(); - const option = { value: true, label: 'Yes' }; - //@ts-ignore - const { wrapper } = setup({ onStarredFilterChange: mockFilterStarred }, mount); - //@ts-ignore - wrapper - .find('Checkbox') - .at(1) - .prop('onChange')(option as any); - + setup({ onStarredFilterChange: mockFilterStarred }); + const checkbox = await screen.findByLabelText(/filter by starred/i); + fireEvent.click(checkbox); expect(mockFilterStarred).toHaveBeenCalledTimes(1); - expect(mockFilterStarred).toHaveBeenCalledWith(option); }); - it('should be called with proper filter option when "filter by tags" is changed', () => { + it('should be called with proper filter option when "filter by tags" is changed', async () => { const mockFilterByTags = jest.fn(); - const tags = [ - { value: 'tag1', label: 'Tag 1' }, - { value: 'tag2', label: 'Tag 2' }, - ]; - //@ts-ignore - const { wrapper } = setup({ onTagFilterChange: mockFilterByTags }, mount); - wrapper - .find({ placeholder: 'Filter by tag' }) - .at(0) - .prop('onChange')([tags[0]]); + setup({ + onTagFilterChange: mockFilterByTags, + query: { ...searchQuery, tag: [] }, + }); + const tagComponent = await screen.findByLabelText('Tag filter'); + + fireEvent.keyDown(tagComponent.querySelector('div') as Node, { keyCode: 40 }); + fireEvent.click(await screen.findByText('tag1')); expect(mockFilterByTags).toHaveBeenCalledTimes(1); expect(mockFilterByTags).toHaveBeenCalledWith(['tag1']); }); diff --git a/public/app/features/search/components/SectionHeader.tsx b/public/app/features/search/components/SectionHeader.tsx index a45752c5545..71e07f6cb0f 100644 --- a/public/app/features/search/components/SectionHeader.tsx +++ b/public/app/features/search/components/SectionHeader.tsx @@ -41,7 +41,11 @@ export const SectionHeader: FC = ({ ); return ( -
+
diff --git a/public/app/features/search/components/mocks.ts b/public/app/features/search/components/mocks.ts deleted file mode 100644 index e9340325eed..00000000000 --- a/public/app/features/search/components/mocks.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const mockSearch = jest.fn(() => { - return Promise.resolve([]); -}); - -jest.mock('app/core/services/search_srv', () => { - return { - SearchSrv: jest.fn().mockImplementation(() => { - return { - search: mockSearch, - getDashboardTags: jest.fn(() => - Promise.resolve([ - { term: 'tag1', count: 2 }, - { term: 'tag2', count: 10 }, - ]) - ), - getSortOptions: jest.fn(() => Promise.resolve({ sortOptions: [{ name: 'test', displayName: 'Test' }] })), - }; - }), - }; -}); diff --git a/public/app/features/search/testData.ts b/public/app/features/search/testData.ts index e92779b9df2..0975d68f8f0 100644 --- a/public/app/features/search/testData.ts +++ b/public/app/features/search/testData.ts @@ -1,51 +1,54 @@ -export const searchResults = [ +import { DashboardSearchItemType, DashboardSection } from './types'; + +export const generalFolder: DashboardSection = { + id: 0, + title: 'General', + items: [ + { + id: 1, + uid: 'lBdLINUWk', + title: 'Test 1', + uri: 'db/test1', + url: '/d/lBdLINUWk/test1', + type: DashboardSearchItemType.DashDB, + tags: [], + isStarred: false, + checked: true, + }, + { + id: 46, + uid: '8DY63kQZk', + title: 'Test 2', + uri: 'db/test2', + url: '/d/8DY63kQZk/test2', + type: DashboardSearchItemType.DashDB, + tags: [], + isStarred: false, + checked: true, + }, + ], + icon: 'folder-open', + score: 1, + expanded: true, + checked: false, + url: '', + type: DashboardSearchItemType.DashFolder, +}; + +export const searchResults: DashboardSection[] = [ { id: 2, uid: 'JB_zdOUWk', title: 'gdev dashboards', expanded: false, - //@ts-ignore items: [], url: '/dashboards/f/JB_zdOUWk/gdev-dashboards', icon: 'folder', score: 0, checked: true, + type: DashboardSearchItemType.DashFolder, }, - { - id: 0, - title: 'General', - items: [ - { - id: 1, - uid: 'lBdLINUWk', - title: 'Test 1', - uri: 'db/test1', - url: '/d/lBdLINUWk/test1', - slug: '', - type: 'dash-db', - //@ts-ignore - tags: [], - isStarred: false, - checked: true, - }, - { - id: 46, - uid: '8DY63kQZk', - title: 'Test 2', - uri: 'db/test2', - url: '/d/8DY63kQZk/test2', - slug: '', - type: 'dash-db', - tags: [], - isStarred: false, - checked: true, - }, - ], - icon: 'folder-open', - score: 1, - expanded: true, - checked: false, - }, + generalFolder, ]; // Search results with more info @@ -59,7 +62,7 @@ export const sections = [ id: 1, uid: 'lBdLINUWk', title: 'Prom dash', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, }, ], }, @@ -75,21 +78,21 @@ export const sections = [ uid: 'OzAIf_rWz', title: 'New dashboard Copy 3', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, }, { id: 46, uid: '8DY63kQZk', title: 'Stocks', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, }, { id: 20, uid: '7MeksYbmk', title: 'Alerting with TestData', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, folderId: 2, }, @@ -97,7 +100,7 @@ export const sections = [ id: 4073, uid: 'j9SHflrWk', title: 'New dashboard Copy 4', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, folderId: 2, }, @@ -111,7 +114,6 @@ export const sections = [ url: '/dashboards/f/JB_zdOUWk/gdev-dashboards', icon: 'folder', score: 2, - //@ts-ignore items: [], }, { @@ -148,21 +150,21 @@ export const sections = [ uri: 'db/new-dashboard-copy', url: '/d/LCFWfl9Zz/new-dashboard-copy', slug: '', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, }, { id: 4072, uid: 'OzAIf_rWz', title: 'New dashboard Copy 3', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: false, }, { id: 1, uid: 'lBdLINUWk', title: 'Prom dash', - type: 'dash-db', + type: DashboardSearchItemType.DashDB, isStarred: true, }, ], diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 26de7b1300c..a7cc34b9a76 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -54,10 +54,6 @@ export interface SearchAction extends Action { payload?: any; } -export interface OpenSearchParams { - query?: string; -} - export interface UidsToDelete { folders: string[]; dashboards: string[];