detail.spec.jsx 24 KB

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