index.spec.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import {
  5. openAddDashboardWidgetModal,
  6. openAddToDashboardModal,
  7. } from 'sentry/actionCreators/modal';
  8. import {NewQuery} from 'sentry/types';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {DisplayModes} from 'sentry/utils/discover/types';
  11. import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboardsV2/types';
  12. import DiscoverBanner from 'sentry/views/eventsV2/banner';
  13. import {ALL_VIEWS} from 'sentry/views/eventsV2/data';
  14. import SavedQueryButtonGroup from 'sentry/views/eventsV2/savedQuery';
  15. import * as utils from 'sentry/views/eventsV2/savedQuery/utils';
  16. const SELECTOR_BUTTON_SAVE_AS = 'button[aria-label="Save as"]';
  17. const SELECTOR_BUTTON_SAVED = '[data-test-id="discover2-savedquery-button-saved"]';
  18. const SELECTOR_BUTTON_UPDATE = '[data-test-id="discover2-savedquery-button-update"]';
  19. const SELECTOR_BUTTON_DELETE = '[data-test-id="discover2-savedquery-button-delete"]';
  20. const SELECTOR_BUTTON_CREATE_ALERT = '[data-test-id="discover2-create-from-discover"]';
  21. jest.mock('sentry/actionCreators/modal');
  22. function mount(
  23. location,
  24. organization,
  25. router,
  26. eventView,
  27. savedQuery,
  28. yAxis,
  29. disabled = false
  30. ) {
  31. return render(
  32. <SavedQueryButtonGroup
  33. location={location}
  34. organization={organization}
  35. eventView={eventView}
  36. savedQuery={savedQuery}
  37. disabled={disabled}
  38. updateCallback={() => {}}
  39. yAxis={yAxis}
  40. onIncompatibleAlertQuery={() => undefined}
  41. router={router}
  42. savedQueryLoading={false}
  43. />
  44. );
  45. }
  46. function generateWrappedComponent(
  47. location,
  48. organization,
  49. router,
  50. eventView,
  51. savedQuery,
  52. yAxis,
  53. disabled = false
  54. ) {
  55. return mountWithTheme(
  56. <SavedQueryButtonGroup
  57. location={location}
  58. organization={organization}
  59. eventView={eventView}
  60. savedQuery={savedQuery}
  61. disabled={disabled}
  62. updateCallback={() => {}}
  63. yAxis={yAxis}
  64. onIncompatibleAlertQuery={() => undefined}
  65. router={router}
  66. savedQueryLoading={false}
  67. />
  68. );
  69. }
  70. describe('EventsV2 > SaveQueryButtonGroup', function () {
  71. const organization = TestStubs.Organization({
  72. features: ['discover-query', 'dashboards-edit'],
  73. });
  74. const location = {
  75. pathname: '/organization/eventsv2/',
  76. query: {},
  77. };
  78. const router = {
  79. location: {query: {}},
  80. };
  81. const yAxis = ['count()', 'failure_count()'];
  82. const errorsQuery = {
  83. ...(ALL_VIEWS.find(view => view.name === 'Errors by Title') as NewQuery),
  84. yAxis: ['count()'],
  85. display: DisplayModes.DEFAULT,
  86. };
  87. const errorsView = EventView.fromSavedQuery(errorsQuery);
  88. const errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
  89. errorsViewSaved.id = '1';
  90. const errorsViewModified = EventView.fromSavedQuery(errorsQuery);
  91. errorsViewModified.id = '1';
  92. errorsViewModified.name = 'Modified Name';
  93. const savedQuery = {
  94. ...errorsViewSaved.toNewQuery(),
  95. yAxis,
  96. dateCreated: '',
  97. dateUpdated: '',
  98. id: '1',
  99. };
  100. afterEach(() => {
  101. MockApiClient.clearMockResponses();
  102. jest.clearAllMocks();
  103. });
  104. describe('building on a new query', () => {
  105. const mockUtils = jest
  106. .spyOn(utils, 'handleCreateQuery')
  107. .mockImplementation(() => Promise.resolve(savedQuery));
  108. beforeEach(() => {
  109. mockUtils.mockClear();
  110. });
  111. it('renders disabled buttons when disabled prop is used', () => {
  112. const wrapper = generateWrappedComponent(
  113. location,
  114. organization,
  115. router,
  116. errorsView,
  117. undefined,
  118. yAxis,
  119. true
  120. );
  121. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  122. expect(buttonSaveAs.props()['aria-disabled']).toBe(true);
  123. });
  124. it('renders the correct set of buttons', () => {
  125. const wrapper = generateWrappedComponent(
  126. location,
  127. organization,
  128. router,
  129. errorsView,
  130. undefined,
  131. yAxis
  132. );
  133. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  134. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  135. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  136. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  137. expect(buttonSaveAs.exists()).toBe(true);
  138. expect(buttonSaved.exists()).toBe(false);
  139. expect(buttonUpdate.exists()).toBe(false);
  140. expect(buttonDelete.exists()).toBe(false);
  141. });
  142. it('hides the banner when save is complete.', async () => {
  143. const wrapper = generateWrappedComponent(
  144. location,
  145. organization,
  146. router,
  147. errorsView,
  148. undefined,
  149. yAxis
  150. );
  151. // Click on ButtonSaveAs to open dropdown
  152. const buttonSaveAs = wrapper.find('DropdownControl').first();
  153. buttonSaveAs.simulate('click');
  154. // Fill in the Input
  155. buttonSaveAs
  156. .find('ButtonSaveInput')
  157. .simulate('change', {target: {value: 'My New Query Name'}}); // currentTarget.value does not work
  158. await tick();
  159. // Click on Save in the Dropdown
  160. await buttonSaveAs.find('ButtonSaveDropDown Button').simulate('click');
  161. // The banner should not render
  162. const banner = mountWithTheme(
  163. <DiscoverBanner organization={organization} resultsUrl="" />
  164. );
  165. expect(banner.find('BannerTitle').exists()).toBe(false);
  166. });
  167. it('saves a well-formed query', async () => {
  168. const wrapper = generateWrappedComponent(
  169. location,
  170. organization,
  171. router,
  172. errorsView,
  173. undefined,
  174. yAxis
  175. );
  176. // Click on ButtonSaveAs to open dropdown
  177. const buttonSaveAs = wrapper.find('DropdownControl').first();
  178. buttonSaveAs.simulate('click');
  179. // Fill in the Input
  180. buttonSaveAs
  181. .find('ButtonSaveInput')
  182. .simulate('change', {target: {value: 'My New Query Name'}}); // currentTarget.value does not work
  183. await tick();
  184. // Click on Save in the Dropdown
  185. await buttonSaveAs.find('ButtonSaveDropDown Button').simulate('click');
  186. expect(mockUtils).toHaveBeenCalledWith(
  187. expect.anything(), // api
  188. organization,
  189. expect.objectContaining({
  190. ...errorsView,
  191. name: 'My New Query Name',
  192. }),
  193. yAxis,
  194. true
  195. );
  196. });
  197. it('rejects if query.name is empty', async () => {
  198. const wrapper = generateWrappedComponent(
  199. location,
  200. organization,
  201. router,
  202. errorsView,
  203. undefined,
  204. yAxis
  205. );
  206. // Click on ButtonSaveAs to open dropdown
  207. const buttonSaveAs = wrapper.find('DropdownControl').first();
  208. buttonSaveAs.simulate('click');
  209. // Do not fill in Input
  210. await tick();
  211. // Click on Save in the Dropdown
  212. buttonSaveAs.find('ButtonSaveDropDown Button').simulate('click');
  213. // Check that EventView has a name
  214. expect(errorsView.name).toBe('Errors by Title');
  215. expect(mockUtils).not.toHaveBeenCalled();
  216. });
  217. });
  218. describe('viewing a saved query', () => {
  219. let mockUtils;
  220. beforeEach(() => {
  221. mockUtils = jest
  222. .spyOn(utils, 'handleDeleteQuery')
  223. .mockImplementation(() => Promise.resolve());
  224. });
  225. afterEach(() => {
  226. mockUtils.mockClear();
  227. });
  228. it('renders the correct set of buttons', () => {
  229. const wrapper = generateWrappedComponent(
  230. location,
  231. organization,
  232. router,
  233. errorsViewSaved,
  234. savedQuery,
  235. yAxis
  236. );
  237. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  238. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  239. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  240. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  241. expect(buttonSaveAs.exists()).toBe(false);
  242. expect(buttonSaved.exists()).toBe(true);
  243. expect(buttonUpdate.exists()).toBe(false);
  244. expect(buttonDelete.exists()).toBe(true);
  245. });
  246. it('treats undefined yAxis the same as count() when checking for changes', () => {
  247. const wrapper = generateWrappedComponent(
  248. location,
  249. organization,
  250. router,
  251. errorsViewSaved,
  252. {...savedQuery, yAxis: undefined},
  253. ['count()']
  254. );
  255. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  256. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  257. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  258. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  259. expect(buttonSaveAs.exists()).toBe(false);
  260. expect(buttonSaved.exists()).toBe(true);
  261. expect(buttonUpdate.exists()).toBe(false);
  262. expect(buttonDelete.exists()).toBe(true);
  263. });
  264. it('converts string yAxis values to array when checking for changes', () => {
  265. const wrapper = generateWrappedComponent(
  266. location,
  267. organization,
  268. router,
  269. errorsViewSaved,
  270. {...savedQuery, yAxis: 'count()'},
  271. ['count()']
  272. );
  273. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  274. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  275. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  276. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  277. expect(buttonSaveAs.exists()).toBe(false);
  278. expect(buttonSaved.exists()).toBe(true);
  279. expect(buttonUpdate.exists()).toBe(false);
  280. expect(buttonDelete.exists()).toBe(true);
  281. });
  282. it('deletes the saved query', async () => {
  283. const wrapper = generateWrappedComponent(
  284. location,
  285. organization,
  286. router,
  287. errorsViewSaved,
  288. savedQuery,
  289. yAxis
  290. );
  291. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE).first();
  292. await buttonDelete.simulate('click');
  293. expect(mockUtils).toHaveBeenCalledWith(
  294. expect.anything(), // api
  295. organization,
  296. expect.objectContaining({id: '1'})
  297. );
  298. });
  299. it('opens a modal with the correct params for top 5 display mode', async function () {
  300. const featuredOrganization = TestStubs.Organization({
  301. features: ['dashboards-edit', 'new-widget-builder-experience-design'],
  302. });
  303. const testData = initializeOrg({
  304. ...initializeOrg(),
  305. organization: featuredOrganization,
  306. });
  307. const savedTopNQuery = TestStubs.DiscoverSavedQuery({
  308. display: DisplayModes.TOP5,
  309. orderby: 'test',
  310. fields: ['test', 'count()'],
  311. topEvents: '2',
  312. });
  313. mount(
  314. testData.router.location,
  315. testData.organization,
  316. testData.router,
  317. EventView.fromSavedQuery(savedTopNQuery),
  318. savedTopNQuery,
  319. ['count()']
  320. );
  321. userEvent.click(screen.getByText('Add to Dashboard'));
  322. expect(openAddDashboardWidgetModal).not.toHaveBeenCalled();
  323. await waitFor(() => {
  324. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  325. expect.objectContaining({
  326. widget: {
  327. title: 'Saved query #1',
  328. displayType: DisplayType.AREA,
  329. limit: 2,
  330. queries: [
  331. {
  332. aggregates: ['count()'],
  333. columns: ['test'],
  334. conditions: '',
  335. fields: ['test', 'count()', 'count()'],
  336. name: '',
  337. orderby: 'test',
  338. },
  339. ],
  340. },
  341. widgetAsQueryParams: expect.objectContaining({
  342. defaultTableColumns: ['test', 'count()'],
  343. defaultTitle: 'Saved query #1',
  344. defaultWidgetQuery:
  345. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  346. displayType: DisplayType.AREA,
  347. source: DashboardWidgetSource.DISCOVERV2,
  348. }),
  349. })
  350. );
  351. });
  352. });
  353. it('opens a modal with the correct params for default display mode', async function () {
  354. const featuredOrganization = TestStubs.Organization({
  355. features: ['dashboards-edit', 'new-widget-builder-experience-design'],
  356. });
  357. const testData = initializeOrg({
  358. ...initializeOrg(),
  359. organization: featuredOrganization,
  360. });
  361. const savedDefaultQuery = TestStubs.DiscoverSavedQuery({
  362. display: DisplayModes.DEFAULT,
  363. orderby: 'count()',
  364. fields: ['test', 'count()'],
  365. yAxis: ['count()'],
  366. });
  367. mount(
  368. testData.router.location,
  369. testData.organization,
  370. testData.router,
  371. EventView.fromSavedQuery(savedDefaultQuery),
  372. savedDefaultQuery,
  373. ['count()']
  374. );
  375. userEvent.click(screen.getByText('Add to Dashboard'));
  376. expect(openAddDashboardWidgetModal).not.toHaveBeenCalled();
  377. await waitFor(() => {
  378. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  379. expect.objectContaining({
  380. widget: {
  381. title: 'Saved query #1',
  382. displayType: DisplayType.LINE,
  383. queries: [
  384. {
  385. aggregates: ['count()'],
  386. columns: [],
  387. conditions: '',
  388. fields: ['count()'],
  389. name: '',
  390. // Orderby gets dropped because ordering only applies to
  391. // Top-N and tables
  392. orderby: '',
  393. },
  394. ],
  395. },
  396. widgetAsQueryParams: expect.objectContaining({
  397. defaultTableColumns: ['test', 'count()'],
  398. defaultTitle: 'Saved query #1',
  399. defaultWidgetQuery:
  400. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  401. displayType: DisplayType.LINE,
  402. source: DashboardWidgetSource.DISCOVERV2,
  403. }),
  404. })
  405. );
  406. });
  407. });
  408. });
  409. describe('modifying a saved query', () => {
  410. let mockUtils;
  411. it('renders the correct set of buttons', () => {
  412. const wrapper = generateWrappedComponent(
  413. location,
  414. organization,
  415. router,
  416. errorsViewModified,
  417. errorsViewSaved.toNewQuery(),
  418. yAxis
  419. );
  420. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  421. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  422. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  423. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  424. expect(buttonSaveAs.exists()).toBe(true);
  425. expect(buttonSaved.exists()).toBe(false);
  426. expect(buttonUpdate.exists()).toBe(true);
  427. expect(buttonDelete.exists()).toBe(true);
  428. });
  429. describe('updates the saved query', () => {
  430. beforeEach(() => {
  431. mockUtils = jest
  432. .spyOn(utils, 'handleUpdateQuery')
  433. .mockImplementation(() => Promise.resolve(savedQuery));
  434. });
  435. afterEach(() => {
  436. mockUtils.mockClear();
  437. });
  438. it('accepts a well-formed query', async () => {
  439. const wrapper = generateWrappedComponent(
  440. location,
  441. organization,
  442. router,
  443. errorsViewModified,
  444. savedQuery,
  445. yAxis
  446. );
  447. // Click on Save in the Dropdown
  448. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE).first();
  449. await buttonUpdate.simulate('click');
  450. expect(mockUtils).toHaveBeenCalledWith(
  451. expect.anything(), // api
  452. organization,
  453. expect.objectContaining({
  454. ...errorsViewModified,
  455. }),
  456. yAxis
  457. );
  458. });
  459. });
  460. describe('creates a separate query', () => {
  461. beforeEach(() => {
  462. mockUtils = jest
  463. .spyOn(utils, 'handleCreateQuery')
  464. .mockImplementation(() => Promise.resolve(savedQuery));
  465. });
  466. afterEach(() => {
  467. mockUtils.mockClear();
  468. });
  469. it('checks that it is forked from a saved query', async () => {
  470. const wrapper = generateWrappedComponent(
  471. location,
  472. organization,
  473. router,
  474. errorsViewModified,
  475. savedQuery,
  476. yAxis
  477. );
  478. // Click on ButtonSaveAs to open dropdown
  479. const buttonSaveAs = wrapper.find('DropdownControl').first();
  480. buttonSaveAs.simulate('click');
  481. // Fill in the Input
  482. buttonSaveAs
  483. .find('ButtonSaveInput')
  484. .simulate('change', {target: {value: 'Forked Query'}});
  485. await tick();
  486. // Click on Save in the Dropdown
  487. await buttonSaveAs.find('ButtonSaveDropDown Button').simulate('click');
  488. expect(mockUtils).toHaveBeenCalledWith(
  489. expect.anything(), // api
  490. organization,
  491. expect.objectContaining({
  492. ...errorsViewModified,
  493. name: 'Forked Query',
  494. }),
  495. yAxis,
  496. false
  497. );
  498. });
  499. });
  500. });
  501. describe('create alert from discover', () => {
  502. it('renders create alert button when metrics alerts is enabled', () => {
  503. const metricAlertOrg = {
  504. ...organization,
  505. features: ['incidents'],
  506. };
  507. const wrapper = generateWrappedComponent(
  508. location,
  509. metricAlertOrg,
  510. router,
  511. errorsViewModified,
  512. savedQuery,
  513. yAxis
  514. );
  515. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  516. expect(buttonCreateAlert.exists()).toBe(true);
  517. });
  518. it('does not render create alert button without metric alerts', () => {
  519. const wrapper = generateWrappedComponent(
  520. location,
  521. organization,
  522. router,
  523. errorsViewModified,
  524. savedQuery,
  525. yAxis
  526. );
  527. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  528. expect(buttonCreateAlert.exists()).toBe(false);
  529. });
  530. });
  531. describe('add dashboard widget', () => {
  532. let initialData;
  533. beforeEach(() => {
  534. initialData = initializeOrg({
  535. organization: {
  536. features: ['discover-query', 'widget-viewer-modal', 'dashboards-edit'],
  537. apdexThreshold: 400,
  538. },
  539. router: {
  540. location: {query: {}},
  541. },
  542. project: 1,
  543. projects: [],
  544. });
  545. MockApiClient.addMockResponse({
  546. url: '/organizations/org-slug/events-stats/',
  547. body: [],
  548. });
  549. MockApiClient.addMockResponse({
  550. url: '/organizations/org-slug/dashboards/',
  551. body: [],
  552. });
  553. });
  554. afterEach(() => {
  555. MockApiClient.clearMockResponses();
  556. });
  557. it('opens widget modal when add to dashboard is clicked', () => {
  558. mount(
  559. initialData.router.location,
  560. initialData.organization,
  561. initialData.router,
  562. errorsViewModified,
  563. savedQuery,
  564. ['count()']
  565. );
  566. userEvent.click(screen.getByText('Add to Dashboard'));
  567. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  568. expect.objectContaining({
  569. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  570. defaultTitle: 'Errors by Title',
  571. defaultWidgetQuery: {
  572. conditions: 'event.type:error',
  573. fields: ['count()'],
  574. aggregates: ['count()'],
  575. columns: [],
  576. name: '',
  577. orderby: '-count()',
  578. },
  579. displayType: 'line',
  580. })
  581. );
  582. });
  583. it('populates dashboard widget modal with saved query data if created from discover', () => {
  584. mount(
  585. initialData.router.location,
  586. initialData.organization,
  587. initialData.router,
  588. errorsViewModified,
  589. savedQuery,
  590. yAxis
  591. );
  592. userEvent.click(screen.getByText('Add to Dashboard'));
  593. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  594. expect.objectContaining({
  595. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  596. defaultTitle: 'Errors by Title',
  597. defaultWidgetQuery: {
  598. conditions: 'event.type:error',
  599. fields: ['count()', 'failure_count()'],
  600. aggregates: ['count()', 'failure_count()'],
  601. columns: [],
  602. name: '',
  603. orderby: '-count()',
  604. },
  605. displayType: 'line',
  606. })
  607. );
  608. });
  609. it('adds equation to query fields if yAxis includes comprising functions', () => {
  610. mount(
  611. initialData.router.location,
  612. initialData.organization,
  613. initialData.router,
  614. errorsViewModified,
  615. savedQuery,
  616. [...yAxis, 'equation|count() + failure_count()']
  617. );
  618. userEvent.click(screen.getByText('Add to Dashboard'));
  619. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  620. expect.objectContaining({
  621. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  622. defaultTitle: 'Errors by Title',
  623. defaultWidgetQuery: {
  624. conditions: 'event.type:error',
  625. fields: ['count()', 'failure_count()', 'equation|count() + failure_count()'],
  626. aggregates: [
  627. 'count()',
  628. 'failure_count()',
  629. 'equation|count() + failure_count()',
  630. ],
  631. columns: [],
  632. name: '',
  633. orderby: '-count()',
  634. },
  635. displayType: 'line',
  636. })
  637. );
  638. });
  639. });
  640. });