dashboardTable.spec.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import {DashboardListItemFixture} from 'sentry-fixture/dashboard';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {UserFixture} from 'sentry-fixture/user';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {
  7. render,
  8. renderGlobalModal,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. within,
  13. } from 'sentry-test/reactTestingLibrary';
  14. import DashboardTable from 'sentry/views/dashboards/manage/dashboardTable';
  15. import {type DashboardListItem, DisplayType} from 'sentry/views/dashboards/types';
  16. describe('Dashboards - DashboardTable', function () {
  17. let dashboards: DashboardListItem[];
  18. let deleteMock: jest.Mock;
  19. let dashboardUpdateMock: jest.Mock;
  20. let createMock: jest.Mock;
  21. const organization = OrganizationFixture({
  22. features: [
  23. 'global-views',
  24. 'dashboards-basic',
  25. 'dashboards-edit',
  26. 'discover-query',
  27. 'dashboards-table-view',
  28. ],
  29. });
  30. const {router} = initializeOrg();
  31. beforeEach(function () {
  32. MockApiClient.clearMockResponses();
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/projects/',
  35. body: [],
  36. });
  37. dashboards = [
  38. DashboardListItemFixture({
  39. id: '1',
  40. title: 'Dashboard 1',
  41. dateCreated: '2021-04-19T13:13:23.962105Z',
  42. createdBy: UserFixture({id: '1'}),
  43. permissions: {
  44. isEditableByEveryone: false,
  45. teamsWithEditAccess: [1],
  46. },
  47. }),
  48. DashboardListItemFixture({
  49. id: '2',
  50. title: 'Dashboard 2',
  51. dateCreated: '2021-04-19T13:13:23.962105Z',
  52. createdBy: UserFixture({id: '1'}),
  53. widgetPreview: [
  54. {
  55. displayType: DisplayType.LINE,
  56. layout: null,
  57. },
  58. {
  59. displayType: DisplayType.TABLE,
  60. layout: null,
  61. },
  62. ],
  63. }),
  64. ];
  65. deleteMock = MockApiClient.addMockResponse({
  66. url: '/organizations/org-slug/dashboards/2/',
  67. method: 'DELETE',
  68. statusCode: 200,
  69. });
  70. MockApiClient.addMockResponse({
  71. url: '/organizations/org-slug/dashboards/2/',
  72. method: 'GET',
  73. statusCode: 200,
  74. body: {
  75. id: '2',
  76. title: 'Dashboard Demo',
  77. widgets: [
  78. {
  79. id: '1',
  80. title: 'Errors',
  81. displayType: 'big_number',
  82. interval: '5m',
  83. },
  84. {
  85. id: '2',
  86. title: 'Transactions',
  87. displayType: 'big_number',
  88. interval: '5m',
  89. },
  90. {
  91. id: '3',
  92. title: 'p50 of /api/cat',
  93. displayType: 'big_number',
  94. interval: '5m',
  95. },
  96. ],
  97. },
  98. });
  99. createMock = MockApiClient.addMockResponse({
  100. url: '/organizations/org-slug/dashboards/',
  101. method: 'POST',
  102. statusCode: 200,
  103. });
  104. dashboardUpdateMock = jest.fn();
  105. });
  106. it('renders an empty list', async function () {
  107. render(
  108. <DashboardTable
  109. onDashboardsChange={jest.fn()}
  110. organization={organization}
  111. dashboards={[]}
  112. location={router.location}
  113. />
  114. );
  115. expect(screen.getByTestId('empty-state')).toBeInTheDocument();
  116. expect(
  117. await screen.findByText('Sorry, no Dashboards match your filters.')
  118. ).toBeInTheDocument();
  119. });
  120. it('renders dashboard list', function () {
  121. render(
  122. <DashboardTable
  123. onDashboardsChange={jest.fn()}
  124. organization={organization}
  125. dashboards={dashboards}
  126. location={router.location}
  127. />
  128. );
  129. expect(screen.getByText('Dashboard 1')).toBeInTheDocument();
  130. expect(screen.getByText('Dashboard 2')).toBeInTheDocument();
  131. });
  132. it('returns landing page url for dashboards', function () {
  133. render(
  134. <DashboardTable
  135. onDashboardsChange={jest.fn()}
  136. organization={organization}
  137. dashboards={dashboards}
  138. location={router.location}
  139. />,
  140. {router}
  141. );
  142. expect(screen.getByRole('link', {name: 'Dashboard 1'})).toHaveAttribute(
  143. 'href',
  144. '/organizations/org-slug/dashboard/1/'
  145. );
  146. expect(screen.getByRole('link', {name: 'Dashboard 2'})).toHaveAttribute(
  147. 'href',
  148. '/organizations/org-slug/dashboard/2/'
  149. );
  150. });
  151. it('persists global selection headers', function () {
  152. render(
  153. <DashboardTable
  154. onDashboardsChange={jest.fn()}
  155. organization={organization}
  156. dashboards={dashboards}
  157. location={{...LocationFixture(), query: {statsPeriod: '7d'}}}
  158. />,
  159. {router}
  160. );
  161. expect(screen.getByRole('link', {name: 'Dashboard 1'})).toHaveAttribute(
  162. 'href',
  163. '/organizations/org-slug/dashboard/1/?statsPeriod=7d'
  164. );
  165. });
  166. it('can delete dashboards', async function () {
  167. render(
  168. <DashboardTable
  169. organization={organization}
  170. dashboards={dashboards}
  171. location={{...LocationFixture(), query: {}}}
  172. onDashboardsChange={dashboardUpdateMock}
  173. />,
  174. {router}
  175. );
  176. renderGlobalModal();
  177. await userEvent.click(screen.getAllByTestId('dashboard-delete')[1]);
  178. expect(deleteMock).not.toHaveBeenCalled();
  179. await userEvent.click(
  180. within(screen.getByRole('dialog')).getByRole('button', {name: /confirm/i})
  181. );
  182. await waitFor(() => {
  183. expect(deleteMock).toHaveBeenCalled();
  184. expect(dashboardUpdateMock).toHaveBeenCalled();
  185. });
  186. });
  187. it('cannot delete last dashboard', function () {
  188. const singleDashboard = [
  189. DashboardListItemFixture({
  190. id: '1',
  191. title: 'Dashboard 1',
  192. dateCreated: '2021-04-19T13:13:23.962105Z',
  193. createdBy: UserFixture({id: '1'}),
  194. widgetPreview: [],
  195. }),
  196. ];
  197. render(
  198. <DashboardTable
  199. organization={organization}
  200. dashboards={singleDashboard}
  201. location={LocationFixture()}
  202. onDashboardsChange={dashboardUpdateMock}
  203. />
  204. );
  205. expect(screen.getAllByTestId('dashboard-delete')[0]).toHaveAttribute(
  206. 'aria-disabled',
  207. 'true'
  208. );
  209. });
  210. it('can duplicate dashboards', async function () {
  211. render(
  212. <DashboardTable
  213. organization={organization}
  214. dashboards={dashboards}
  215. location={{...LocationFixture(), query: {}}}
  216. onDashboardsChange={dashboardUpdateMock}
  217. />
  218. );
  219. renderGlobalModal();
  220. await userEvent.click(screen.getAllByTestId('dashboard-duplicate')[1]);
  221. expect(createMock).not.toHaveBeenCalled();
  222. await userEvent.click(
  223. within(screen.getByRole('dialog')).getByRole('button', {name: /confirm/i})
  224. );
  225. await waitFor(() => {
  226. expect(createMock).toHaveBeenCalled();
  227. expect(dashboardUpdateMock).toHaveBeenCalled();
  228. });
  229. });
  230. it('does not throw an error if the POST fails during duplication', async function () {
  231. const postMock = MockApiClient.addMockResponse({
  232. url: '/organizations/org-slug/dashboards/',
  233. method: 'POST',
  234. statusCode: 404,
  235. });
  236. render(
  237. <DashboardTable
  238. organization={organization}
  239. dashboards={dashboards}
  240. location={{...LocationFixture(), query: {}}}
  241. onDashboardsChange={dashboardUpdateMock}
  242. />
  243. );
  244. renderGlobalModal();
  245. await userEvent.click(screen.getAllByTestId('dashboard-duplicate')[1]);
  246. expect(postMock).not.toHaveBeenCalled();
  247. await userEvent.click(
  248. within(screen.getByRole('dialog')).getByRole('button', {name: /confirm/i})
  249. );
  250. await waitFor(() => {
  251. expect(postMock).toHaveBeenCalled();
  252. // Should not update, and not throw error
  253. expect(dashboardUpdateMock).not.toHaveBeenCalled();
  254. });
  255. });
  256. it('renders access column', async function () {
  257. const organizationWithEditAccess = OrganizationFixture({
  258. features: [
  259. 'global-views',
  260. 'dashboards-basic',
  261. 'dashboards-edit',
  262. 'discover-query',
  263. 'dashboards-table-view',
  264. 'dashboards-edit-access',
  265. ],
  266. });
  267. render(
  268. <DashboardTable
  269. onDashboardsChange={jest.fn()}
  270. organization={organizationWithEditAccess}
  271. dashboards={dashboards}
  272. location={router.location}
  273. />
  274. );
  275. expect((await screen.findAllByTestId('grid-head-cell')).length).toBe(5);
  276. expect(screen.getByText('Access')).toBeInTheDocument();
  277. await userEvent.click((await screen.findAllByTestId('edit-access-dropdown'))[0]);
  278. expect(screen.getAllByPlaceholderText('Search Teams')[0]).toBeInTheDocument();
  279. });
  280. });