detail.spec.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import {act} from 'react-dom/test-utils';
  2. import {browserHistory} from 'react-router';
  3. import {createListeners} from 'sentry-test/createListeners';
  4. import {mountWithTheme} from 'sentry-test/enzyme';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {mountGlobalModal} from 'sentry-test/modal';
  7. import ViewEditDashboard from 'app/views/dashboardsV2/view';
  8. describe('Dashboards > Detail', function () {
  9. const organization = TestStubs.Organization({
  10. features: ['global-views', 'dashboards-basic', 'dashboards-edit', 'discover-query'],
  11. projects: [TestStubs.Project()],
  12. });
  13. describe('prebuilt dashboards', function () {
  14. let wrapper;
  15. let initialData, mockVisit;
  16. beforeEach(function () {
  17. initialData = initializeOrg({organization});
  18. MockApiClient.addMockResponse({
  19. url: '/organizations/org-slug/tags/',
  20. body: [],
  21. });
  22. MockApiClient.addMockResponse({
  23. url: '/organizations/org-slug/projects/',
  24. body: [TestStubs.Project()],
  25. });
  26. MockApiClient.addMockResponse({
  27. url: '/organizations/org-slug/dashboards/',
  28. body: [
  29. TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  30. TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  31. ],
  32. });
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/dashboards/default-overview/',
  35. body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  36. });
  37. mockVisit = MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/dashboards/1/visit/',
  39. method: 'POST',
  40. body: [],
  41. statusCode: 200,
  42. });
  43. });
  44. afterEach(function () {
  45. MockApiClient.clearMockResponses();
  46. });
  47. it('can delete', async function () {
  48. const deleteMock = MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/dashboards/default-overview/',
  50. method: 'DELETE',
  51. });
  52. wrapper = mountWithTheme(
  53. <ViewEditDashboard
  54. organization={initialData.organization}
  55. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  56. router={initialData.router}
  57. location={location}
  58. />,
  59. initialData.routerContext
  60. );
  61. await tick();
  62. wrapper.update();
  63. // Enter edit mode.
  64. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  65. const modal = await mountGlobalModal();
  66. // Click delete, confirm will show
  67. wrapper.find('Controls Button[data-test-id="dashboard-delete"]').simulate('click');
  68. await tick();
  69. await modal.update();
  70. // Click confirm
  71. modal.find('button[aria-label="Confirm"]').simulate('click');
  72. expect(deleteMock).toHaveBeenCalled();
  73. });
  74. it('can rename and save', async function () {
  75. const fireEvent = createListeners('window');
  76. const updateMock = MockApiClient.addMockResponse({
  77. url: '/organizations/org-slug/dashboards/default-overview/',
  78. method: 'PUT',
  79. body: TestStubs.Dashboard([], {id: '8', title: 'Updated prebuilt'}),
  80. });
  81. wrapper = mountWithTheme(
  82. <ViewEditDashboard
  83. organization={initialData.organization}
  84. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  85. router={initialData.router}
  86. location={initialData.router.location}
  87. />,
  88. initialData.routerContext
  89. );
  90. await tick();
  91. wrapper.update();
  92. // Enter edit mode.
  93. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  94. // Rename
  95. const dashboardTitle = wrapper.find('DashboardTitle Label');
  96. dashboardTitle.simulate('click');
  97. wrapper.find('StyledInput').simulate('change', {
  98. target: {innerText: 'Updated prebuilt', value: 'Updated prebuilt'},
  99. });
  100. act(() => {
  101. // Press enter
  102. fireEvent.keyDown('Enter');
  103. });
  104. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  105. await tick();
  106. expect(updateMock).toHaveBeenCalledWith(
  107. '/organizations/org-slug/dashboards/default-overview/',
  108. expect.objectContaining({
  109. data: expect.objectContaining({title: 'Updated prebuilt'}),
  110. })
  111. );
  112. // Should redirect to the new dashboard.
  113. expect(browserHistory.replace).toHaveBeenCalledWith(
  114. expect.objectContaining({
  115. pathname: '/organizations/org-slug/dashboard/8/',
  116. })
  117. );
  118. });
  119. it('disables buttons based on features', async function () {
  120. initialData = initializeOrg({
  121. organization: TestStubs.Organization({
  122. features: ['global-views', 'dashboards-basic', 'discover-query'],
  123. projects: [TestStubs.Project()],
  124. }),
  125. });
  126. wrapper = mountWithTheme(
  127. <ViewEditDashboard
  128. organization={initialData.organization}
  129. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  130. router={initialData.router}
  131. location={initialData.router.location}
  132. />,
  133. initialData.routerContext
  134. );
  135. await tick();
  136. wrapper.update();
  137. // Edit should be disabled
  138. const editProps = wrapper
  139. .find('Controls Button[data-test-id="dashboard-edit"]')
  140. .props();
  141. expect(editProps.disabled).toBe(true);
  142. expect(mockVisit).not.toHaveBeenCalled();
  143. });
  144. });
  145. describe('custom dashboards', function () {
  146. let wrapper, initialData, widgets, mockVisit;
  147. beforeEach(function () {
  148. initialData = initializeOrg({organization});
  149. widgets = [
  150. TestStubs.Widget(
  151. [{name: '', conditions: 'event.type:error', fields: ['count()']}],
  152. {
  153. title: 'Errors',
  154. interval: '1d',
  155. id: '1',
  156. }
  157. ),
  158. TestStubs.Widget(
  159. [{name: '', conditions: 'event.type:transaction', fields: ['count()']}],
  160. {
  161. title: 'Transactions',
  162. interval: '1d',
  163. id: '2',
  164. }
  165. ),
  166. TestStubs.Widget(
  167. [
  168. {
  169. name: '',
  170. conditions: 'event.type:transaction transaction:/api/cats',
  171. fields: ['p50()'],
  172. },
  173. ],
  174. {
  175. title: 'p50 of /api/cats',
  176. interval: '1d',
  177. id: '3',
  178. }
  179. ),
  180. ];
  181. mockVisit = MockApiClient.addMockResponse({
  182. url: '/organizations/org-slug/dashboards/1/visit/',
  183. method: 'POST',
  184. body: [],
  185. statusCode: 200,
  186. });
  187. MockApiClient.addMockResponse({
  188. url: '/organizations/org-slug/tags/',
  189. body: [],
  190. });
  191. MockApiClient.addMockResponse({
  192. url: '/organizations/org-slug/projects/',
  193. body: [TestStubs.Project()],
  194. });
  195. MockApiClient.addMockResponse({
  196. url: '/organizations/org-slug/dashboards/',
  197. body: [
  198. TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  199. TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  200. ],
  201. });
  202. MockApiClient.addMockResponse({
  203. url: '/organizations/org-slug/dashboards/1/',
  204. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  205. });
  206. MockApiClient.addMockResponse({
  207. url: '/organizations/org-slug/events-stats/',
  208. body: {data: []},
  209. });
  210. });
  211. afterEach(function () {
  212. MockApiClient.clearMockResponses();
  213. });
  214. it('can remove widgets', async function () {
  215. const updateMock = MockApiClient.addMockResponse({
  216. url: '/organizations/org-slug/dashboards/1/',
  217. method: 'PUT',
  218. });
  219. wrapper = mountWithTheme(
  220. <ViewEditDashboard
  221. organization={initialData.organization}
  222. params={{orgId: 'org-slug', dashboardId: '1'}}
  223. router={initialData.router}
  224. location={initialData.router.location}
  225. />,
  226. initialData.routerContext
  227. );
  228. await tick();
  229. wrapper.update();
  230. expect(mockVisit).toHaveBeenCalledTimes(1);
  231. // Enter edit mode.
  232. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  233. const card = wrapper.find('WidgetCard').first();
  234. card.find('StyledPanel').simulate('mouseOver');
  235. // Remove the second and third widgets
  236. wrapper
  237. .find('WidgetCard')
  238. .at(1)
  239. .find('IconClick[data-test-id="widget-delete"]')
  240. .simulate('click');
  241. wrapper
  242. .find('WidgetCard')
  243. .at(1)
  244. .find('IconClick[data-test-id="widget-delete"]')
  245. .simulate('click');
  246. // Save changes
  247. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  248. expect(updateMock).toHaveBeenCalled();
  249. expect(updateMock).toHaveBeenCalledWith(
  250. '/organizations/org-slug/dashboards/1/',
  251. expect.objectContaining({
  252. data: expect.objectContaining({
  253. title: 'Custom Errors',
  254. widgets: [widgets[0]],
  255. }),
  256. })
  257. );
  258. // Visit should not be called again on dashboard update
  259. expect(mockVisit).toHaveBeenCalledTimes(1);
  260. });
  261. it('can enter edit mode for widgets', async function () {
  262. wrapper = mountWithTheme(
  263. <ViewEditDashboard
  264. organization={initialData.organization}
  265. params={{orgId: 'org-slug', dashboardId: '1'}}
  266. router={initialData.router}
  267. location={initialData.router.location}
  268. />,
  269. initialData.routerContext
  270. );
  271. await tick();
  272. wrapper.update();
  273. // Enter edit mode.
  274. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  275. const card = wrapper.find('WidgetCard').first();
  276. card.find('StyledPanel').simulate('mouseOver');
  277. // Edit the first widget
  278. wrapper
  279. .find('WidgetCard')
  280. .first()
  281. .find('IconClick[data-test-id="widget-edit"]')
  282. .simulate('click');
  283. await tick();
  284. await wrapper.update();
  285. const modal = await mountGlobalModal();
  286. expect(modal.find('AddDashboardWidgetModal').props().widget).toEqual(widgets[0]);
  287. });
  288. it('hides and shows breadcrumbs based on feature', async function () {
  289. const newOrg = initializeOrg({
  290. organization: TestStubs.Organization({
  291. features: ['global-views', 'dashboards-basic', 'discover-query'],
  292. projects: [TestStubs.Project()],
  293. }),
  294. });
  295. wrapper = mountWithTheme(
  296. <ViewEditDashboard
  297. organization={newOrg.organization}
  298. params={{orgId: 'org-slug', dashboardId: '1'}}
  299. router={newOrg.router}
  300. location={newOrg.router.location}
  301. />,
  302. newOrg.routerContext
  303. );
  304. await tick();
  305. wrapper.update();
  306. expect(wrapper.find('Breadcrumbs').exists()).toBe(false);
  307. wrapper = mountWithTheme(
  308. <ViewEditDashboard
  309. organization={initialData.organization}
  310. params={{orgId: 'org-slug', dashboardId: '1'}}
  311. router={initialData.router}
  312. location={initialData.router.location}
  313. />,
  314. initialData.routerContext
  315. );
  316. await tick();
  317. wrapper.update();
  318. const breadcrumbs = wrapper.find('Breadcrumbs');
  319. expect(breadcrumbs.exists()).toBe(true);
  320. expect(breadcrumbs.find('BreadcrumbLink').find('a').text()).toEqual('Dashboards');
  321. expect(breadcrumbs.find('BreadcrumbItem').last().text()).toEqual('Custom Errors');
  322. });
  323. });
  324. });