Browse.test.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { render, RenderResult, waitFor, within } from '@testing-library/react';
  2. import userEvent from '@testing-library/user-event';
  3. import React from 'react';
  4. import { Provider } from 'react-redux';
  5. import { Router } from 'react-router-dom';
  6. import { PluginType } from '@grafana/data';
  7. import { locationService } from '@grafana/runtime';
  8. import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
  9. import { configureStore } from 'app/store/configureStore';
  10. import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
  11. import { fetchRemotePlugins } from '../state/actions';
  12. import { PluginAdminRoutes, CatalogPlugin, ReducerState, RequestStatus } from '../types';
  13. import BrowsePage from './Browse';
  14. jest.mock('@grafana/runtime', () => {
  15. const original = jest.requireActual('@grafana/runtime');
  16. const mockedRuntime = { ...original };
  17. mockedRuntime.config.bootData.user.isGrafanaAdmin = true;
  18. mockedRuntime.config.buildInfo.version = 'v8.1.0';
  19. return mockedRuntime;
  20. });
  21. const renderBrowse = (
  22. path = '/plugins',
  23. plugins: CatalogPlugin[] = [],
  24. pluginsStateOverride?: ReducerState
  25. ): RenderResult => {
  26. const store = configureStore({ plugins: pluginsStateOverride || getPluginsStateMock(plugins) });
  27. locationService.push(path);
  28. const props = getRouteComponentProps({
  29. route: { routeName: PluginAdminRoutes.Home } as any,
  30. });
  31. return render(
  32. <Provider store={store}>
  33. <Router history={locationService.getHistory()}>
  34. <BrowsePage {...props} />
  35. </Router>
  36. </Provider>
  37. );
  38. };
  39. describe('Browse list of plugins', () => {
  40. describe('when filtering', () => {
  41. it('should list installed plugins by default', async () => {
  42. const { queryByText } = renderBrowse('/plugins', [
  43. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  44. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true }),
  45. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  46. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: false }),
  47. ]);
  48. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  49. expect(queryByText('Plugin 1')).toBeInTheDocument();
  50. expect(queryByText('Plugin 2')).toBeInTheDocument();
  51. expect(queryByText('Plugin 3')).toBeInTheDocument();
  52. expect(queryByText('Plugin 4')).toBeNull();
  53. });
  54. it('should list all plugins (except core plugins) when filtering by all', async () => {
  55. const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=all', [
  56. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  57. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
  58. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  59. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isCore: true }),
  60. ]);
  61. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  62. expect(queryByText('Plugin 2')).toBeInTheDocument();
  63. expect(queryByText('Plugin 3')).toBeInTheDocument();
  64. // Core plugins should not be listed
  65. expect(queryByText('Plugin 4')).not.toBeInTheDocument();
  66. });
  67. it('should list installed plugins (including core plugins) when filtering by installed', async () => {
  68. const { queryByText } = renderBrowse('/plugins?filterBy=installed', [
  69. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  70. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
  71. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  72. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isCore: true }),
  73. ]);
  74. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  75. expect(queryByText('Plugin 3')).toBeInTheDocument();
  76. expect(queryByText('Plugin 4')).toBeInTheDocument();
  77. // Not showing not installed plugins
  78. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  79. });
  80. it('should list all plugins (including disabled plugins) when filtering by all', async () => {
  81. const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=all', [
  82. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  83. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
  84. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  85. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isDisabled: true }),
  86. ]);
  87. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  88. expect(queryByText('Plugin 2')).toBeInTheDocument();
  89. expect(queryByText('Plugin 3')).toBeInTheDocument();
  90. expect(queryByText('Plugin 4')).toBeInTheDocument();
  91. });
  92. it('should list installed plugins (including disabled plugins) when filtering by installed', async () => {
  93. const { queryByText } = renderBrowse('/plugins?filterBy=installed', [
  94. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  95. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
  96. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  97. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isDisabled: true }),
  98. ]);
  99. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  100. expect(queryByText('Plugin 3')).toBeInTheDocument();
  101. expect(queryByText('Plugin 4')).toBeInTheDocument();
  102. // Not showing not installed plugins
  103. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  104. });
  105. it('should list enterprise plugins when querying for them', async () => {
  106. const { queryByText } = renderBrowse('/plugins?filterBy=all&q=wavefront', [
  107. getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront', isInstalled: true, isEnterprise: true }),
  108. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true, isCore: true }),
  109. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true }),
  110. ]);
  111. await waitFor(() => expect(queryByText('Wavefront')).toBeInTheDocument());
  112. // Should not show plugins that don't match the query
  113. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  114. expect(queryByText('Plugin 3')).not.toBeInTheDocument();
  115. });
  116. it('should list only datasource plugins when filtering by datasource', async () => {
  117. const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=datasource', [
  118. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
  119. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
  120. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
  121. ]);
  122. await waitFor(() => expect(queryByText('Plugin 2')).toBeInTheDocument());
  123. // Other plugin types shouldn't be shown
  124. expect(queryByText('Plugin 1')).not.toBeInTheDocument();
  125. expect(queryByText('Plugin 3')).not.toBeInTheDocument();
  126. });
  127. it('should list only panel plugins when filtering by panel', async () => {
  128. const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=panel', [
  129. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
  130. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
  131. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
  132. ]);
  133. await waitFor(() => expect(queryByText('Plugin 3')).toBeInTheDocument());
  134. // Other plugin types shouldn't be shown
  135. expect(queryByText('Plugin 1')).not.toBeInTheDocument();
  136. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  137. });
  138. it('should list only app plugins when filtering by app', async () => {
  139. const { queryByText } = renderBrowse('/plugins?filterBy=all&filterByType=app', [
  140. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
  141. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
  142. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.panel }),
  143. ]);
  144. await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
  145. // Other plugin types shouldn't be shown
  146. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  147. expect(queryByText('Plugin 3')).not.toBeInTheDocument();
  148. });
  149. });
  150. describe('when searching', () => {
  151. it('should only list plugins matching search', async () => {
  152. const { queryByText } = renderBrowse('/plugins?filterBy=all&q=zabbix', [
  153. getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
  154. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }),
  155. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }),
  156. ]);
  157. await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
  158. // Other plugin types shouldn't be shown
  159. expect(queryByText('Plugin 2')).not.toBeInTheDocument();
  160. expect(queryByText('Plugin 3')).not.toBeInTheDocument();
  161. });
  162. });
  163. describe('when sorting', () => {
  164. it('should sort plugins by name in ascending alphabetical order', async () => {
  165. const { findByTestId } = renderBrowse('/plugins?filterBy=all', [
  166. getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront' }),
  167. getCatalogPluginMock({ id: 'redis-application', name: 'Redis Application' }),
  168. getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
  169. getCatalogPluginMock({ id: 'diagram', name: 'Diagram' }),
  170. getCatalogPluginMock({ id: 'acesvg', name: 'ACE.SVG' }),
  171. ]);
  172. const pluginList = await findByTestId('plugin-list');
  173. const pluginHeadings = within(pluginList).queryAllByRole('heading');
  174. expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
  175. 'ACE.SVG',
  176. 'Diagram',
  177. 'Redis Application',
  178. 'Wavefront',
  179. 'Zabbix',
  180. ]);
  181. });
  182. it('should sort plugins by name in descending alphabetical order', async () => {
  183. const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=nameDesc', [
  184. getCatalogPluginMock({ id: 'wavefront', name: 'Wavefront' }),
  185. getCatalogPluginMock({ id: 'redis-application', name: 'Redis Application' }),
  186. getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
  187. getCatalogPluginMock({ id: 'diagram', name: 'Diagram' }),
  188. getCatalogPluginMock({ id: 'acesvg', name: 'ACE.SVG' }),
  189. ]);
  190. const pluginList = await findByTestId('plugin-list');
  191. const pluginHeadings = within(pluginList).queryAllByRole('heading');
  192. expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
  193. 'Zabbix',
  194. 'Wavefront',
  195. 'Redis Application',
  196. 'Diagram',
  197. 'ACE.SVG',
  198. ]);
  199. });
  200. it('should sort plugins by date in ascending updated order', async () => {
  201. const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=updated', [
  202. getCatalogPluginMock({ id: '1', name: 'Wavefront', updatedAt: '2021-04-01T00:00:00.000Z' }),
  203. getCatalogPluginMock({ id: '2', name: 'Redis Application', updatedAt: '2021-02-01T00:00:00.000Z' }),
  204. getCatalogPluginMock({ id: '3', name: 'Zabbix', updatedAt: '2021-01-01T00:00:00.000Z' }),
  205. getCatalogPluginMock({ id: '4', name: 'Diagram', updatedAt: '2021-05-01T00:00:00.000Z' }),
  206. getCatalogPluginMock({ id: '5', name: 'ACE.SVG', updatedAt: '2021-02-01T00:00:00.000Z' }),
  207. ]);
  208. const pluginList = await findByTestId('plugin-list');
  209. const pluginHeadings = within(pluginList).queryAllByRole('heading');
  210. expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
  211. 'Diagram',
  212. 'Wavefront',
  213. 'Redis Application',
  214. 'ACE.SVG',
  215. 'Zabbix',
  216. ]);
  217. });
  218. it('should sort plugins by date in ascending published order', async () => {
  219. const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=published', [
  220. getCatalogPluginMock({ id: '1', name: 'Wavefront', publishedAt: '2021-04-01T00:00:00.000Z' }),
  221. getCatalogPluginMock({ id: '2', name: 'Redis Application', publishedAt: '2021-02-01T00:00:00.000Z' }),
  222. getCatalogPluginMock({ id: '3', name: 'Zabbix', publishedAt: '2021-01-01T00:00:00.000Z' }),
  223. getCatalogPluginMock({ id: '4', name: 'Diagram', publishedAt: '2021-05-01T00:00:00.000Z' }),
  224. getCatalogPluginMock({ id: '5', name: 'ACE.SVG', publishedAt: '2021-02-01T00:00:00.000Z' }),
  225. ]);
  226. const pluginList = await findByTestId('plugin-list');
  227. const pluginHeadings = within(pluginList).queryAllByRole('heading');
  228. expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
  229. 'Diagram',
  230. 'Wavefront',
  231. 'Redis Application',
  232. 'ACE.SVG',
  233. 'Zabbix',
  234. ]);
  235. });
  236. it('should sort plugins by number of downloads in ascending order', async () => {
  237. const { findByTestId } = renderBrowse('/plugins?filterBy=all&sortBy=downloads', [
  238. getCatalogPluginMock({ id: '1', name: 'Wavefront', downloads: 30 }),
  239. getCatalogPluginMock({ id: '2', name: 'Redis Application', downloads: 10 }),
  240. getCatalogPluginMock({ id: '3', name: 'Zabbix', downloads: 50 }),
  241. getCatalogPluginMock({ id: '4', name: 'Diagram', downloads: 20 }),
  242. getCatalogPluginMock({ id: '5', name: 'ACE.SVG', downloads: 40 }),
  243. ]);
  244. const pluginList = await findByTestId('plugin-list');
  245. const pluginHeadings = within(pluginList).queryAllByRole('heading');
  246. expect(pluginHeadings.map((heading) => heading.innerHTML)).toStrictEqual([
  247. 'Zabbix',
  248. 'ACE.SVG',
  249. 'Wavefront',
  250. 'Diagram',
  251. 'Redis Application',
  252. ]);
  253. });
  254. });
  255. describe('when GCOM api is not available', () => {
  256. it('should disable the All / Installed filter', async () => {
  257. const plugins = [
  258. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true }),
  259. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 2', isInstalled: true }),
  260. getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 3', isInstalled: true }),
  261. ];
  262. const state = getPluginsStateMock(plugins);
  263. // Mock the store like if the remote plugins request was rejected
  264. const stateOverride = {
  265. ...state,
  266. requests: {
  267. ...state.requests,
  268. [fetchRemotePlugins.typePrefix]: {
  269. status: RequestStatus.Rejected,
  270. },
  271. },
  272. };
  273. // The radio input for the filters should be disabled
  274. const { getByRole } = renderBrowse('/plugins', [], stateOverride);
  275. await waitFor(() => expect(getByRole('radio', { name: 'Installed' })).toBeDisabled());
  276. });
  277. });
  278. it('should be possible to switch between display modes', async () => {
  279. const { findByTestId, getByRole, getByTitle, queryByText } = renderBrowse('/plugins?filterBy=all', [
  280. getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1' }),
  281. getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }),
  282. getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }),
  283. ]);
  284. await findByTestId('plugin-list');
  285. const listOptionTitle = 'Display plugins in list';
  286. const gridOptionTitle = 'Display plugins in a grid layout';
  287. const listOption = getByRole('radio', { name: listOptionTitle });
  288. const listOptionLabel = getByTitle(listOptionTitle);
  289. const gridOption = getByRole('radio', { name: gridOptionTitle });
  290. const gridOptionLabel = getByTitle(gridOptionTitle);
  291. // All options should be visible
  292. expect(listOptionLabel).toBeVisible();
  293. expect(gridOptionLabel).toBeVisible();
  294. // The default display mode should be "grid"
  295. expect(gridOption).toBeChecked();
  296. expect(listOption).not.toBeChecked();
  297. // Switch to "list" view
  298. await userEvent.click(listOption);
  299. expect(gridOption).not.toBeChecked();
  300. expect(listOption).toBeChecked();
  301. // All plugins are still visible
  302. expect(queryByText('Plugin 1')).toBeInTheDocument();
  303. expect(queryByText('Plugin 2')).toBeInTheDocument();
  304. expect(queryByText('Plugin 3')).toBeInTheDocument();
  305. });
  306. });