detail.spec.jsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. import {browserHistory} from 'react-router';
  2. import {createListeners} from 'sentry-test/createListeners';
  3. import {selectDropdownMenuItem} from 'sentry-test/dropdownMenu';
  4. import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {mountGlobalModal} from 'sentry-test/modal';
  7. import {act} from 'sentry-test/reactTestingLibrary';
  8. import {triggerPress} from 'sentry-test/utils';
  9. import * as modals from 'sentry/actionCreators/modal';
  10. import ProjectsStore from 'sentry/stores/projectsStore';
  11. import {DashboardState} from 'sentry/views/dashboardsV2/types';
  12. import * as types from 'sentry/views/dashboardsV2/types';
  13. import ViewEditDashboard from 'sentry/views/dashboardsV2/view';
  14. import {OrganizationContext} from 'sentry/views/organizationContext';
  15. describe('Dashboards > Detail', function () {
  16. enforceActOnUseLegacyStoreHook();
  17. const organization = TestStubs.Organization({
  18. features: ['global-views', 'dashboards-basic', 'dashboards-edit', 'discover-query'],
  19. });
  20. const projects = [TestStubs.Project()];
  21. describe('prebuilt dashboards', function () {
  22. let wrapper;
  23. let initialData, mockVisit;
  24. beforeEach(function () {
  25. act(() => ProjectsStore.loadInitialData(projects));
  26. initialData = initializeOrg({organization});
  27. MockApiClient.addMockResponse({
  28. url: '/organizations/org-slug/tags/',
  29. body: [],
  30. });
  31. MockApiClient.addMockResponse({
  32. url: '/organizations/org-slug/projects/',
  33. body: [TestStubs.Project()],
  34. });
  35. MockApiClient.addMockResponse({
  36. url: '/organizations/org-slug/dashboards/',
  37. body: [
  38. TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  39. TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  40. ],
  41. });
  42. MockApiClient.addMockResponse({
  43. url: '/organizations/org-slug/dashboards/default-overview/',
  44. body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  45. });
  46. mockVisit = MockApiClient.addMockResponse({
  47. url: '/organizations/org-slug/dashboards/1/visit/',
  48. method: 'POST',
  49. body: [],
  50. statusCode: 200,
  51. });
  52. MockApiClient.addMockResponse({
  53. url: '/organizations/org-slug/users/',
  54. method: 'GET',
  55. body: [],
  56. });
  57. });
  58. afterEach(function () {
  59. MockApiClient.clearMockResponses();
  60. if (wrapper) {
  61. wrapper.unmount();
  62. }
  63. });
  64. it('can delete', async function () {
  65. const deleteMock = MockApiClient.addMockResponse({
  66. url: '/organizations/org-slug/dashboards/default-overview/',
  67. method: 'DELETE',
  68. });
  69. wrapper = mountWithTheme(
  70. <OrganizationContext.Provider value={initialData.organization}>
  71. <ViewEditDashboard
  72. organization={initialData.organization}
  73. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  74. router={initialData.router}
  75. location={initialData.router.location}
  76. />
  77. </OrganizationContext.Provider>,
  78. initialData.routerContext
  79. );
  80. await tick();
  81. wrapper.update();
  82. // Enter edit mode.
  83. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  84. const modal = await mountGlobalModal();
  85. // Click delete, confirm will show
  86. wrapper.find('Controls Button[data-test-id="dashboard-delete"]').simulate('click');
  87. await tick();
  88. await modal.update();
  89. // Click confirm
  90. modal.find('button[aria-label="Confirm"]').simulate('click');
  91. expect(deleteMock).toHaveBeenCalled();
  92. });
  93. it('can rename and save', async function () {
  94. const fireEvent = createListeners('window');
  95. const updateMock = MockApiClient.addMockResponse({
  96. url: '/organizations/org-slug/dashboards/default-overview/',
  97. method: 'PUT',
  98. body: TestStubs.Dashboard([], {id: '8', title: 'Updated prebuilt'}),
  99. });
  100. wrapper = mountWithTheme(
  101. <OrganizationContext.Provider value={initialData.organization}>
  102. <ViewEditDashboard
  103. organization={initialData.organization}
  104. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  105. router={initialData.router}
  106. location={initialData.router.location}
  107. />
  108. </OrganizationContext.Provider>,
  109. initialData.routerContext
  110. );
  111. await tick();
  112. wrapper.update();
  113. // Enter edit mode.
  114. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  115. await tick();
  116. wrapper.update();
  117. // Rename
  118. const dashboardTitle = wrapper.find('DashboardTitle Label');
  119. dashboardTitle.simulate('click');
  120. wrapper.find('StyledInput').simulate('change', {
  121. target: {innerText: 'Updated prebuilt', value: 'Updated prebuilt'},
  122. });
  123. act(() => {
  124. // Press enter
  125. fireEvent.keyDown('Enter');
  126. });
  127. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  128. await tick();
  129. wrapper.update();
  130. expect(updateMock).toHaveBeenCalledWith(
  131. '/organizations/org-slug/dashboards/default-overview/',
  132. expect.objectContaining({
  133. data: expect.objectContaining({title: 'Updated prebuilt'}),
  134. })
  135. );
  136. // Should redirect to the new dashboard.
  137. expect(browserHistory.replace).toHaveBeenCalledWith(
  138. expect.objectContaining({
  139. pathname: '/organizations/org-slug/dashboard/8/',
  140. })
  141. );
  142. });
  143. it('disables buttons based on features', async function () {
  144. initialData = initializeOrg({
  145. organization: TestStubs.Organization({
  146. features: ['global-views', 'dashboards-basic', 'discover-query'],
  147. projects: [TestStubs.Project()],
  148. }),
  149. });
  150. wrapper = mountWithTheme(
  151. <OrganizationContext.Provider value={initialData.organization}>
  152. <ViewEditDashboard
  153. organization={initialData.organization}
  154. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  155. router={initialData.router}
  156. location={initialData.router.location}
  157. />
  158. </OrganizationContext.Provider>,
  159. initialData.routerContext
  160. );
  161. await tick();
  162. wrapper.update();
  163. // Edit should be disabled
  164. const editProps = wrapper
  165. .find('Controls Button[data-test-id="dashboard-edit"]')
  166. .props();
  167. expect(editProps.disabled).toBe(true);
  168. expect(mockVisit).not.toHaveBeenCalled();
  169. });
  170. });
  171. describe('custom dashboards', function () {
  172. let wrapper, initialData, widgets, mockVisit;
  173. const openEditModal = jest.spyOn(modals, 'openAddDashboardWidgetModal');
  174. beforeEach(function () {
  175. initialData = initializeOrg({organization});
  176. types.MAX_WIDGETS = 30;
  177. widgets = [
  178. TestStubs.Widget(
  179. [
  180. {
  181. name: '',
  182. conditions: 'event.type:error',
  183. fields: ['count()'],
  184. aggregates: ['count()'],
  185. columns: [],
  186. },
  187. ],
  188. {
  189. title: 'Errors',
  190. interval: '1d',
  191. id: '1',
  192. }
  193. ),
  194. TestStubs.Widget(
  195. [
  196. {
  197. name: '',
  198. conditions: 'event.type:transaction',
  199. fields: ['count()'],
  200. aggregates: ['count()'],
  201. columns: [],
  202. },
  203. ],
  204. {
  205. title: 'Transactions',
  206. interval: '1d',
  207. id: '2',
  208. }
  209. ),
  210. TestStubs.Widget(
  211. [
  212. {
  213. name: '',
  214. conditions: 'event.type:transaction transaction:/api/cats',
  215. fields: ['p50()'],
  216. aggregates: ['p50()'],
  217. columns: [],
  218. },
  219. ],
  220. {
  221. title: 'p50 of /api/cats',
  222. interval: '1d',
  223. id: '3',
  224. }
  225. ),
  226. ];
  227. mockVisit = MockApiClient.addMockResponse({
  228. url: '/organizations/org-slug/dashboards/1/visit/',
  229. method: 'POST',
  230. body: [],
  231. statusCode: 200,
  232. });
  233. MockApiClient.addMockResponse({
  234. url: '/organizations/org-slug/tags/',
  235. body: [],
  236. });
  237. MockApiClient.addMockResponse({
  238. url: '/organizations/org-slug/projects/',
  239. body: [TestStubs.Project()],
  240. });
  241. MockApiClient.addMockResponse({
  242. url: '/organizations/org-slug/dashboards/',
  243. body: [
  244. TestStubs.Dashboard([], {
  245. id: 'default-overview',
  246. title: 'Default',
  247. widgetDisplay: ['area'],
  248. }),
  249. TestStubs.Dashboard([], {
  250. id: '1',
  251. title: 'Custom Errors',
  252. widgetDisplay: ['area'],
  253. }),
  254. ],
  255. });
  256. MockApiClient.addMockResponse({
  257. method: 'GET',
  258. url: '/organizations/org-slug/dashboards/1/',
  259. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  260. });
  261. MockApiClient.addMockResponse({
  262. url: '/organizations/org-slug/dashboards/1/',
  263. method: 'PUT',
  264. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  265. });
  266. MockApiClient.addMockResponse({
  267. url: '/organizations/org-slug/events-stats/',
  268. body: {data: []},
  269. });
  270. MockApiClient.addMockResponse({
  271. method: 'POST',
  272. url: '/organizations/org-slug/dashboards/widgets/',
  273. body: [],
  274. });
  275. MockApiClient.addMockResponse({
  276. method: 'GET',
  277. url: '/organizations/org-slug/recent-searches/',
  278. body: [],
  279. });
  280. MockApiClient.addMockResponse({
  281. method: 'GET',
  282. url: '/organizations/org-slug/issues/',
  283. body: [],
  284. });
  285. MockApiClient.addMockResponse({
  286. url: '/organizations/org-slug/eventsv2/',
  287. method: 'GET',
  288. body: [],
  289. });
  290. MockApiClient.addMockResponse({
  291. url: '/organizations/org-slug/users/',
  292. method: 'GET',
  293. body: [],
  294. });
  295. });
  296. afterEach(function () {
  297. MockApiClient.clearMockResponses();
  298. jest.clearAllMocks();
  299. if (wrapper) {
  300. wrapper.unmount();
  301. }
  302. });
  303. it('can remove widgets', async function () {
  304. const updateMock = MockApiClient.addMockResponse({
  305. url: '/organizations/org-slug/dashboards/1/',
  306. method: 'PUT',
  307. body: TestStubs.Dashboard([widgets[0]], {id: '1', title: 'Custom Errors'}),
  308. });
  309. wrapper = mountWithTheme(
  310. <OrganizationContext.Provider value={initialData.organization}>
  311. <ViewEditDashboard
  312. organization={initialData.organization}
  313. params={{orgId: 'org-slug', dashboardId: '1'}}
  314. router={initialData.router}
  315. location={initialData.router.location}
  316. />
  317. </OrganizationContext.Provider>,
  318. initialData.routerContext
  319. );
  320. await tick();
  321. wrapper.update();
  322. expect(mockVisit).toHaveBeenCalledTimes(1);
  323. // Enter edit mode.
  324. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  325. const card = wrapper.find('WidgetCard').first();
  326. card.find('WidgetCardPanel').simulate('mouseOver');
  327. // Remove the second and third widgets
  328. wrapper
  329. .find('WidgetCard')
  330. .at(1)
  331. .find('Button[data-test-id="widget-delete"]')
  332. .simulate('click');
  333. wrapper
  334. .find('WidgetCard')
  335. .at(1)
  336. .find('Button[data-test-id="widget-delete"]')
  337. .simulate('click');
  338. // Save changes
  339. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  340. await tick();
  341. expect(updateMock).toHaveBeenCalled();
  342. expect(updateMock).toHaveBeenCalledWith(
  343. '/organizations/org-slug/dashboards/1/',
  344. expect.objectContaining({
  345. data: expect.objectContaining({
  346. title: 'Custom Errors',
  347. widgets: [widgets[0]],
  348. }),
  349. })
  350. );
  351. // Visit should not be called again on dashboard update
  352. expect(mockVisit).toHaveBeenCalledTimes(1);
  353. });
  354. it('opens edit modal for widgets', async function () {
  355. wrapper = mountWithTheme(
  356. <OrganizationContext.Provider value={initialData.organization}>
  357. <ViewEditDashboard
  358. organization={initialData.organization}
  359. params={{orgId: 'org-slug', dashboardId: '1'}}
  360. router={initialData.router}
  361. location={initialData.router.location}
  362. />
  363. </OrganizationContext.Provider>,
  364. initialData.routerContext
  365. );
  366. await tick();
  367. wrapper.update();
  368. // Enter edit mode.
  369. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  370. wrapper.update();
  371. const card = wrapper.find('WidgetCard').first();
  372. card.find('WidgetCardPanel').simulate('mouseOver');
  373. // Edit the first widget
  374. wrapper
  375. .find('WidgetCard')
  376. .first()
  377. .find('Button[data-test-id="widget-edit"]')
  378. .simulate('click');
  379. expect(openEditModal).toHaveBeenCalledTimes(1);
  380. expect(openEditModal).toHaveBeenCalledWith(
  381. expect.objectContaining({
  382. widget: {
  383. id: '1',
  384. interval: '1d',
  385. queries: [
  386. {
  387. conditions: 'event.type:error',
  388. fields: ['count()'],
  389. aggregates: ['count()'],
  390. columns: [],
  391. name: '',
  392. },
  393. ],
  394. title: 'Errors',
  395. type: 'line',
  396. },
  397. })
  398. );
  399. });
  400. it('does not update if api update fails', async function () {
  401. const fireEvent = createListeners('window');
  402. window.confirm = jest.fn(() => true);
  403. MockApiClient.addMockResponse({
  404. url: '/organizations/org-slug/dashboards/1/',
  405. method: 'PUT',
  406. statusCode: 400,
  407. });
  408. wrapper = mountWithTheme(
  409. <OrganizationContext.Provider value={initialData.organization}>
  410. <ViewEditDashboard
  411. organization={initialData.organization}
  412. params={{orgId: 'org-slug', dashboardId: '1'}}
  413. router={initialData.router}
  414. location={initialData.router.location}
  415. />
  416. </OrganizationContext.Provider>,
  417. initialData.routerContext
  418. );
  419. await tick();
  420. wrapper.update();
  421. // Enter edit mode.
  422. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  423. // Rename
  424. const dashboardTitle = wrapper.find('DashboardTitle Label');
  425. dashboardTitle.simulate('click');
  426. wrapper.find('StyledInput').simulate('change', {
  427. target: {innerText: 'Updated Name', value: 'Updated Name'},
  428. });
  429. act(() => {
  430. // Press enter
  431. fireEvent.keyDown('Enter');
  432. });
  433. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  434. await tick();
  435. wrapper.update();
  436. expect(wrapper.find('DashboardTitle EditableText').props().value).toEqual(
  437. 'Updated Name'
  438. );
  439. wrapper.find('Controls Button[data-test-id="dashboard-cancel"]').simulate('click');
  440. expect(wrapper.find('DashboardTitle EditableText').props().value).toEqual(
  441. 'Custom Errors'
  442. );
  443. });
  444. it('shows add widget option', async function () {
  445. wrapper = mountWithTheme(
  446. <OrganizationContext.Provider value={initialData.organization}>
  447. <ViewEditDashboard
  448. organization={initialData.organization}
  449. params={{orgId: 'org-slug', dashboardId: '1'}}
  450. router={initialData.router}
  451. location={initialData.router.location}
  452. />
  453. </OrganizationContext.Provider>,
  454. initialData.routerContext
  455. );
  456. await tick();
  457. wrapper.update();
  458. // Enter edit mode.
  459. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  460. wrapper.update();
  461. expect(wrapper.find('AddWidget').exists()).toBe(true);
  462. });
  463. it('opens custom modal when add widget option is clicked', async function () {
  464. wrapper = mountWithTheme(
  465. <OrganizationContext.Provider value={initialData.organization}>
  466. <ViewEditDashboard
  467. organization={initialData.organization}
  468. params={{orgId: 'org-slug', dashboardId: '1'}}
  469. router={initialData.router}
  470. location={initialData.router.location}
  471. />
  472. </OrganizationContext.Provider>,
  473. initialData.routerContext
  474. );
  475. await tick();
  476. wrapper.update();
  477. // Enter edit mode.
  478. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  479. wrapper.update();
  480. wrapper.find('AddButton[data-test-id="widget-add"]').simulate('click');
  481. expect(openEditModal).toHaveBeenCalledTimes(1);
  482. });
  483. it('opens widget library when add widget option is clicked', async function () {
  484. initialData = initializeOrg({
  485. organization: TestStubs.Organization({
  486. features: [
  487. 'global-views',
  488. 'dashboards-basic',
  489. 'dashboards-edit',
  490. 'discover-query',
  491. ],
  492. projects: [TestStubs.Project()],
  493. }),
  494. });
  495. wrapper = mountWithTheme(
  496. <OrganizationContext.Provider value={initialData.organization}>
  497. <ViewEditDashboard
  498. organization={initialData.organization}
  499. params={{orgId: 'org-slug', dashboardId: '1'}}
  500. router={initialData.router}
  501. location={initialData.router.location}
  502. />
  503. </OrganizationContext.Provider>,
  504. initialData.routerContext
  505. );
  506. await tick();
  507. wrapper.update();
  508. // Enter edit mode.
  509. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  510. wrapper.update();
  511. wrapper.find('AddButton[data-test-id="widget-add"]').simulate('click');
  512. expect(openEditModal).toHaveBeenCalledTimes(1);
  513. expect(openEditModal).toHaveBeenCalledWith(
  514. expect.objectContaining({
  515. source: types.DashboardWidgetSource.LIBRARY,
  516. })
  517. );
  518. });
  519. it('hides add widget option', async function () {
  520. types.MAX_WIDGETS = 1;
  521. wrapper = mountWithTheme(
  522. <OrganizationContext.Provider value={initialData.organization}>
  523. <ViewEditDashboard
  524. organization={initialData.organization}
  525. params={{orgId: 'org-slug', dashboardId: '1'}}
  526. router={initialData.router}
  527. location={initialData.router.location}
  528. />
  529. </OrganizationContext.Provider>,
  530. initialData.routerContext
  531. );
  532. await tick();
  533. wrapper.update();
  534. // Enter edit mode.
  535. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  536. wrapper.update();
  537. expect(wrapper.find('AddWidget').exists()).toBe(false);
  538. });
  539. it('hides and shows breadcrumbs based on feature', async function () {
  540. const newOrg = initializeOrg({
  541. organization: TestStubs.Organization({
  542. features: ['global-views', 'dashboards-basic', 'discover-query'],
  543. projects: [TestStubs.Project()],
  544. }),
  545. });
  546. wrapper = mountWithTheme(
  547. <OrganizationContext.Provider value={initialData.organization}>
  548. <ViewEditDashboard
  549. organization={newOrg.organization}
  550. params={{orgId: 'org-slug', dashboardId: '1'}}
  551. router={newOrg.router}
  552. location={newOrg.router.location}
  553. />
  554. </OrganizationContext.Provider>,
  555. newOrg.routerContext
  556. );
  557. await tick();
  558. wrapper.update();
  559. expect(wrapper.find('Breadcrumbs').exists()).toBe(false);
  560. wrapper = mountWithTheme(
  561. <OrganizationContext.Provider value={initialData.organization}>
  562. <ViewEditDashboard
  563. organization={initialData.organization}
  564. params={{orgId: 'org-slug', dashboardId: '1'}}
  565. router={initialData.router}
  566. location={initialData.router.location}
  567. />
  568. </OrganizationContext.Provider>,
  569. initialData.routerContext
  570. );
  571. await tick();
  572. wrapper.update();
  573. const breadcrumbs = wrapper.find('Breadcrumbs');
  574. expect(breadcrumbs.exists()).toBe(true);
  575. expect(breadcrumbs.find('BreadcrumbLink').find('a').text()).toEqual('Dashboards');
  576. expect(breadcrumbs.find('BreadcrumbItem').last().text()).toEqual('Custom Errors');
  577. });
  578. it('unsets newWidget after rendering', async function () {
  579. initialData.router.location = {
  580. query: {
  581. displayType: 'line',
  582. interval: '5m',
  583. queryConditions: ['title:test', 'event.type:test'],
  584. queryFields: ['count()', 'failure_count()'],
  585. queryNames: ['1', '2'],
  586. queryOrderby: '',
  587. title: 'Widget Title',
  588. },
  589. };
  590. wrapper = mountWithTheme(
  591. <OrganizationContext.Provider value={initialData.organization}>
  592. <ViewEditDashboard
  593. organization={initialData.organization}
  594. params={{orgId: 'org-slug', dashboardId: '1'}}
  595. router={initialData.router}
  596. location={{...initialData.router.location, pathname: '/mockpath'}}
  597. />
  598. </OrganizationContext.Provider>,
  599. initialData.routerContext
  600. );
  601. expect(wrapper.find('DashboardDetail').props().initialState).toEqual(
  602. DashboardState.EDIT
  603. );
  604. expect(wrapper.find('DashboardDetail').props().newWidget).toBeDefined();
  605. await act(async () => {
  606. // Wrap await tick in act because componentDidMount in Dashboard triggers
  607. // state change when parsing widget from location
  608. await tick();
  609. });
  610. wrapper.update();
  611. // The newWidget state was cleared after adding the widget
  612. expect(wrapper.find('DashboardDetail').props().newWidget).toBeUndefined();
  613. await act(async () => {
  614. await tick();
  615. });
  616. });
  617. it('enters edit mode when given a new widget in location query', async function () {
  618. initialData.router.location = {
  619. query: {
  620. displayType: 'line',
  621. interval: '5m',
  622. queryConditions: ['title:test', 'event.type:test'],
  623. queryFields: ['count()', 'failure_count()'],
  624. queryNames: ['1', '2'],
  625. queryOrderby: '',
  626. title: 'Widget Title',
  627. },
  628. };
  629. wrapper = mountWithTheme(
  630. <OrganizationContext.Provider value={initialData.organization}>
  631. <ViewEditDashboard
  632. organization={initialData.organization}
  633. params={{orgId: 'org-slug', dashboardId: '1'}}
  634. router={initialData.router}
  635. location={{...initialData.router.location, pathname: '/mockpath'}}
  636. />
  637. </OrganizationContext.Provider>,
  638. initialData.routerContext
  639. );
  640. expect(wrapper.find('DashboardDetail').props().initialState).toEqual(
  641. DashboardState.EDIT
  642. );
  643. await act(async () => {
  644. // Wrap await tick in act because componentDidMount in Dashboard triggers
  645. // state change when parsing widget from location
  646. await tick();
  647. });
  648. });
  649. it('enters view mode when not given a new widget in location query', async function () {
  650. wrapper = mountWithTheme(
  651. <OrganizationContext.Provider value={initialData.organization}>
  652. <ViewEditDashboard
  653. organization={initialData.organization}
  654. params={{orgId: 'org-slug', dashboardId: '1'}}
  655. router={initialData.router}
  656. location={initialData.router.location}
  657. />
  658. </OrganizationContext.Provider>,
  659. initialData.routerContext
  660. );
  661. await tick();
  662. wrapper.update();
  663. expect(wrapper.find('DashboardDetail').props().initialState).toEqual(
  664. DashboardState.VIEW
  665. );
  666. });
  667. it('opens add widget to custom modal', async function () {
  668. types.MAX_WIDGETS = 10;
  669. initialData = initializeOrg({
  670. organization: TestStubs.Organization({
  671. features: [
  672. 'global-views',
  673. 'dashboards-basic',
  674. 'dashboards-edit',
  675. 'discover-query',
  676. ],
  677. projects: [TestStubs.Project()],
  678. }),
  679. });
  680. wrapper = mountWithTheme(
  681. <OrganizationContext.Provider value={initialData.organization}>
  682. <ViewEditDashboard
  683. organization={initialData.organization}
  684. params={{orgId: 'org-slug', dashboardId: '1'}}
  685. router={initialData.router}
  686. location={initialData.router.location}
  687. />
  688. </OrganizationContext.Provider>,
  689. initialData.routerContext
  690. );
  691. await tick();
  692. wrapper.update();
  693. expect(wrapper.find('Controls Tooltip').prop('disabled')).toBe(true);
  694. // Enter Add Widget mode
  695. wrapper
  696. .find('Controls Button[data-test-id="add-widget-library"]')
  697. .simulate('click');
  698. expect(openEditModal).toHaveBeenCalledTimes(1);
  699. expect(openEditModal).toHaveBeenCalledWith(
  700. expect.objectContaining({
  701. source: types.DashboardWidgetSource.LIBRARY,
  702. })
  703. );
  704. });
  705. it('disables add library widgets when max widgets reached', async function () {
  706. types.MAX_WIDGETS = 3;
  707. initialData = initializeOrg({
  708. organization: TestStubs.Organization({
  709. features: [
  710. 'global-views',
  711. 'dashboards-basic',
  712. 'dashboards-edit',
  713. 'discover-query',
  714. ],
  715. projects: [TestStubs.Project()],
  716. }),
  717. });
  718. wrapper = mountWithTheme(
  719. <OrganizationContext.Provider value={initialData.organization}>
  720. <ViewEditDashboard
  721. organization={initialData.organization}
  722. params={{orgId: 'org-slug', dashboardId: '1'}}
  723. router={initialData.router}
  724. location={initialData.router.location}
  725. />
  726. </OrganizationContext.Provider>,
  727. initialData.routerContext
  728. );
  729. await tick();
  730. wrapper.update();
  731. expect(wrapper.find('WidgetCard')).toHaveLength(3);
  732. expect(
  733. wrapper.find('Controls Button[data-test-id="add-widget-library"]').props()
  734. .disabled
  735. ).toEqual(true);
  736. expect(wrapper.find('Controls Tooltip').prop('disabled')).toBe(false);
  737. await act(async () => {
  738. triggerPress(wrapper.first().find('MenuControlWrap Button').first());
  739. await tick();
  740. wrapper.update();
  741. });
  742. expect(
  743. wrapper.find(`MenuItemWrap[data-test-id="duplicate-widget"]`).props()[
  744. 'aria-disabled'
  745. ]
  746. ).toEqual(true);
  747. });
  748. it('opens edit modal when editing widget from context menu', async function () {
  749. wrapper = mountWithTheme(
  750. <OrganizationContext.Provider value={initialData.organization}>
  751. <ViewEditDashboard
  752. organization={initialData.organization}
  753. params={{orgId: 'org-slug', dashboardId: '1'}}
  754. router={initialData.router}
  755. location={initialData.router.location}
  756. />
  757. </OrganizationContext.Provider>,
  758. initialData.routerContext
  759. );
  760. await tick();
  761. wrapper.update();
  762. expect(wrapper.find('WidgetCard')).toHaveLength(3);
  763. await selectDropdownMenuItem({
  764. wrapper,
  765. specifiers: {prefix: 'WidgetCard', first: true},
  766. itemKey: 'edit-widget',
  767. });
  768. expect(openEditModal).toHaveBeenCalledTimes(1);
  769. expect(openEditModal).toHaveBeenCalledWith(
  770. expect.objectContaining({
  771. widget: {
  772. id: '1',
  773. interval: '1d',
  774. queries: [
  775. {
  776. conditions: 'event.type:error',
  777. fields: ['count()'],
  778. aggregates: ['count()'],
  779. columns: [],
  780. name: '',
  781. },
  782. ],
  783. title: 'Errors',
  784. type: 'line',
  785. },
  786. })
  787. );
  788. });
  789. });
  790. });