index.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {
  4. render,
  5. renderGlobalModal,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import * as modal from 'sentry/actionCreators/modal';
  11. import * as LineChart from 'sentry/components/charts/lineChart';
  12. import SimpleTableChart from 'sentry/components/charts/simpleTableChart';
  13. import {MINUTE, SECOND} from 'sentry/utils/formatters';
  14. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  15. import type {Widget} from 'sentry/views/dashboards/types';
  16. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  17. import WidgetCard from 'sentry/views/dashboards/widgetCard';
  18. import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries';
  19. jest.mock('sentry/components/charts/simpleTableChart', () => jest.fn(() => <div />));
  20. jest.mock('sentry/views/dashboards/widgetCard/releaseWidgetQueries');
  21. jest.mock('sentry/components/lazyRender', () => ({
  22. LazyRender: ({children}: {children: React.ReactNode}) => children,
  23. }));
  24. describe('Dashboards > WidgetCard', function () {
  25. const {router, organization} = initializeOrg({
  26. organization: OrganizationFixture({
  27. features: ['dashboards-edit', 'discover-basic'],
  28. }),
  29. router: {orgId: 'orgId'},
  30. } as Parameters<typeof initializeOrg>[0]);
  31. const renderWithProviders = (component: React.ReactNode) =>
  32. render(
  33. <MEPSettingProvider forceTransactions={false}>{component}</MEPSettingProvider>,
  34. {organization, router}
  35. );
  36. const multipleQueryWidget: Widget = {
  37. title: 'Errors',
  38. description: 'Valid widget description',
  39. interval: '5m',
  40. displayType: DisplayType.LINE,
  41. widgetType: WidgetType.DISCOVER,
  42. queries: [
  43. {
  44. conditions: 'event.type:error',
  45. fields: ['count()', 'failure_count()'],
  46. aggregates: ['count()', 'failure_count()'],
  47. columns: [],
  48. name: 'errors',
  49. orderby: '',
  50. },
  51. {
  52. conditions: 'event.type:default',
  53. fields: ['count()', 'failure_count()'],
  54. aggregates: ['count()', 'failure_count()'],
  55. columns: [],
  56. name: 'default',
  57. orderby: '',
  58. },
  59. ],
  60. };
  61. const selection = {
  62. projects: [1],
  63. environments: ['prod'],
  64. datetime: {
  65. period: '14d',
  66. start: null,
  67. end: null,
  68. utc: false,
  69. },
  70. };
  71. const api = new MockApiClient();
  72. let eventsMock;
  73. beforeEach(function () {
  74. MockApiClient.addMockResponse({
  75. url: '/organizations/org-slug/events-stats/',
  76. body: {meta: {isMetricsData: false}},
  77. });
  78. eventsMock = MockApiClient.addMockResponse({
  79. url: '/organizations/org-slug/events/',
  80. body: {
  81. meta: {fields: {title: 'string'}},
  82. data: [{title: 'title'}],
  83. },
  84. });
  85. });
  86. afterEach(function () {
  87. MockApiClient.clearMockResponses();
  88. });
  89. it('renders with Open in Discover button and opens the Query Selector Modal when clicked', async function () {
  90. const spy = jest.spyOn(modal, 'openDashboardWidgetQuerySelectorModal');
  91. renderWithProviders(
  92. <WidgetCard
  93. api={api}
  94. widget={multipleQueryWidget}
  95. selection={selection}
  96. isEditingDashboard={false}
  97. onDelete={() => undefined}
  98. onEdit={() => undefined}
  99. onDuplicate={() => undefined}
  100. renderErrorMessage={() => undefined}
  101. showContextMenu
  102. widgetLimitReached={false}
  103. />
  104. );
  105. await userEvent.click(await screen.findByLabelText('Widget actions'));
  106. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  107. await userEvent.click(screen.getByText('Open in Discover'));
  108. expect(spy).toHaveBeenCalledWith({
  109. isMetricsData: false,
  110. organization,
  111. widget: multipleQueryWidget,
  112. });
  113. });
  114. it('renders with Open in Discover button and opens in Discover when clicked', async function () {
  115. renderWithProviders(
  116. <WidgetCard
  117. api={api}
  118. widget={{...multipleQueryWidget, queries: [multipleQueryWidget.queries[0]]}}
  119. selection={selection}
  120. isEditingDashboard={false}
  121. onDelete={() => undefined}
  122. onEdit={() => undefined}
  123. onDuplicate={() => undefined}
  124. renderErrorMessage={() => undefined}
  125. showContextMenu
  126. widgetLimitReached={false}
  127. />
  128. );
  129. await userEvent.click(await screen.findByLabelText('Widget actions'));
  130. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  131. await userEvent.click(screen.getByText('Open in Discover'));
  132. expect(router.push).toHaveBeenCalledWith(
  133. '/organizations/org-slug/discover/results/?environment=prod&field=count%28%29&field=failure_count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29&yAxis=failure_count%28%29'
  134. );
  135. });
  136. it('renders widget description in dashboard', async function () {
  137. renderWithProviders(
  138. <WidgetCard
  139. api={api}
  140. widget={multipleQueryWidget}
  141. selection={selection}
  142. isEditingDashboard={false}
  143. onDelete={() => undefined}
  144. onEdit={() => undefined}
  145. onDuplicate={() => undefined}
  146. renderErrorMessage={() => undefined}
  147. showContextMenu
  148. widgetLimitReached={false}
  149. />
  150. );
  151. expect(await screen.findByText('Valid widget description')).toBeInTheDocument();
  152. });
  153. it('Opens in Discover with prepended fields pulled from equations', async function () {
  154. renderWithProviders(
  155. <WidgetCard
  156. api={api}
  157. widget={{
  158. ...multipleQueryWidget,
  159. queries: [
  160. {
  161. ...multipleQueryWidget.queries[0],
  162. fields: [
  163. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  164. ],
  165. columns: [],
  166. aggregates: [
  167. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  168. ],
  169. },
  170. ],
  171. }}
  172. selection={selection}
  173. isEditingDashboard={false}
  174. onDelete={() => undefined}
  175. onEdit={() => undefined}
  176. onDuplicate={() => undefined}
  177. renderErrorMessage={() => undefined}
  178. showContextMenu
  179. widgetLimitReached={false}
  180. />
  181. );
  182. await userEvent.click(await screen.findByLabelText('Widget actions'));
  183. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  184. await userEvent.click(screen.getByText('Open in Discover'));
  185. expect(router.push).toHaveBeenCalledWith(
  186. '/organizations/org-slug/discover/results/?environment=prod&field=count_if%28transaction.duration%2Cequals%2C300%29&field=failure_count%28%29&field=count%28%29&field=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29'
  187. );
  188. });
  189. it('Opens in Discover with Top N', async function () {
  190. renderWithProviders(
  191. <WidgetCard
  192. api={api}
  193. widget={{
  194. ...multipleQueryWidget,
  195. displayType: DisplayType.TOP_N,
  196. queries: [
  197. {
  198. ...multipleQueryWidget.queries[0],
  199. fields: ['transaction', 'count()'],
  200. columns: ['transaction'],
  201. aggregates: ['count()'],
  202. },
  203. ],
  204. }}
  205. selection={selection}
  206. isEditingDashboard={false}
  207. onDelete={() => undefined}
  208. onEdit={() => undefined}
  209. onDuplicate={() => undefined}
  210. renderErrorMessage={() => undefined}
  211. showContextMenu
  212. widgetLimitReached={false}
  213. />
  214. );
  215. await userEvent.click(await screen.findByLabelText('Widget actions'));
  216. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  217. await userEvent.click(screen.getByText('Open in Discover'));
  218. expect(router.push).toHaveBeenCalledWith(
  219. '/organizations/org-slug/discover/results/?display=top5&environment=prod&field=transaction&field=count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29'
  220. );
  221. });
  222. it('allows Open in Discover when the widget contains custom measurements', async function () {
  223. renderWithProviders(
  224. <WidgetCard
  225. api={api}
  226. widget={{
  227. ...multipleQueryWidget,
  228. displayType: DisplayType.LINE,
  229. queries: [
  230. {
  231. ...multipleQueryWidget.queries[0],
  232. conditions: '',
  233. fields: [],
  234. columns: [],
  235. aggregates: ['p99(measurements.custom.measurement)'],
  236. },
  237. ],
  238. }}
  239. selection={selection}
  240. isEditingDashboard={false}
  241. onDelete={() => undefined}
  242. onEdit={() => undefined}
  243. onDuplicate={() => undefined}
  244. renderErrorMessage={() => undefined}
  245. showContextMenu
  246. widgetLimitReached={false}
  247. />
  248. );
  249. await userEvent.click(await screen.findByLabelText('Widget actions'));
  250. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  251. await userEvent.click(screen.getByText('Open in Discover'));
  252. expect(router.push).toHaveBeenCalledWith(
  253. '/organizations/org-slug/discover/results/?environment=prod&field=p99%28measurements.custom.measurement%29&name=Errors&project=1&query=&statsPeriod=14d&yAxis=p99%28measurements.custom.measurement%29'
  254. );
  255. });
  256. it('calls onDuplicate when Duplicate Widget is clicked', async function () {
  257. const mock = jest.fn();
  258. renderWithProviders(
  259. <WidgetCard
  260. api={api}
  261. widget={{
  262. ...multipleQueryWidget,
  263. displayType: DisplayType.AREA,
  264. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  265. }}
  266. selection={selection}
  267. isEditingDashboard={false}
  268. onDelete={() => undefined}
  269. onEdit={() => undefined}
  270. onDuplicate={mock}
  271. renderErrorMessage={() => undefined}
  272. showContextMenu
  273. widgetLimitReached={false}
  274. />
  275. );
  276. await userEvent.click(await screen.findByLabelText('Widget actions'));
  277. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  278. await userEvent.click(screen.getByText('Duplicate Widget'));
  279. expect(mock).toHaveBeenCalledTimes(1);
  280. });
  281. it('does not add duplicate widgets if max widget is reached', async function () {
  282. const mock = jest.fn();
  283. renderWithProviders(
  284. <WidgetCard
  285. api={api}
  286. widget={{
  287. ...multipleQueryWidget,
  288. displayType: DisplayType.AREA,
  289. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  290. }}
  291. selection={selection}
  292. isEditingDashboard={false}
  293. onDelete={() => undefined}
  294. onEdit={() => undefined}
  295. onDuplicate={mock}
  296. renderErrorMessage={() => undefined}
  297. showContextMenu
  298. widgetLimitReached
  299. />
  300. );
  301. await userEvent.click(await screen.findByLabelText('Widget actions'));
  302. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  303. await userEvent.click(screen.getByText('Duplicate Widget'));
  304. expect(mock).toHaveBeenCalledTimes(0);
  305. });
  306. it('calls onEdit when Edit Widget is clicked', async function () {
  307. const mock = jest.fn();
  308. renderWithProviders(
  309. <WidgetCard
  310. api={api}
  311. widget={{
  312. ...multipleQueryWidget,
  313. displayType: DisplayType.AREA,
  314. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  315. }}
  316. selection={selection}
  317. isEditingDashboard={false}
  318. onDelete={() => undefined}
  319. onEdit={mock}
  320. onDuplicate={() => undefined}
  321. renderErrorMessage={() => undefined}
  322. showContextMenu
  323. widgetLimitReached={false}
  324. />
  325. );
  326. await userEvent.click(await screen.findByLabelText('Widget actions'));
  327. expect(screen.getByText('Edit Widget')).toBeInTheDocument();
  328. await userEvent.click(screen.getByText('Edit Widget'));
  329. expect(mock).toHaveBeenCalledTimes(1);
  330. });
  331. it('renders delete widget option', async function () {
  332. const mock = jest.fn();
  333. renderWithProviders(
  334. <WidgetCard
  335. api={api}
  336. widget={{
  337. ...multipleQueryWidget,
  338. displayType: DisplayType.AREA,
  339. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  340. }}
  341. selection={selection}
  342. isEditingDashboard={false}
  343. onDelete={mock}
  344. onEdit={() => undefined}
  345. onDuplicate={() => undefined}
  346. renderErrorMessage={() => undefined}
  347. showContextMenu
  348. widgetLimitReached={false}
  349. />
  350. );
  351. await userEvent.click(await screen.findByLabelText('Widget actions'));
  352. expect(screen.getByText('Delete Widget')).toBeInTheDocument();
  353. await userEvent.click(screen.getByText('Delete Widget'));
  354. // Confirm Modal
  355. renderGlobalModal();
  356. await screen.findByRole('dialog');
  357. await userEvent.click(screen.getByTestId('confirm-button'));
  358. expect(mock).toHaveBeenCalled();
  359. });
  360. it('calls events with a limit of 20 items', async function () {
  361. const mock = jest.fn();
  362. renderWithProviders(
  363. <WidgetCard
  364. api={api}
  365. widget={{
  366. ...multipleQueryWidget,
  367. displayType: DisplayType.TABLE,
  368. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  369. }}
  370. selection={selection}
  371. isEditingDashboard={false}
  372. onDelete={mock}
  373. onEdit={() => undefined}
  374. onDuplicate={() => undefined}
  375. renderErrorMessage={() => undefined}
  376. showContextMenu
  377. widgetLimitReached={false}
  378. tableItemLimit={20}
  379. />
  380. );
  381. await waitFor(() => {
  382. expect(eventsMock).toHaveBeenCalledWith(
  383. '/organizations/org-slug/events/',
  384. expect.objectContaining({
  385. query: expect.objectContaining({
  386. per_page: 20,
  387. }),
  388. })
  389. );
  390. });
  391. });
  392. it('calls events with a default limit of 5 items', async function () {
  393. const mock = jest.fn();
  394. renderWithProviders(
  395. <WidgetCard
  396. api={api}
  397. widget={{
  398. ...multipleQueryWidget,
  399. displayType: DisplayType.TABLE,
  400. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  401. }}
  402. selection={selection}
  403. isEditingDashboard={false}
  404. onDelete={mock}
  405. onEdit={() => undefined}
  406. onDuplicate={() => undefined}
  407. renderErrorMessage={() => undefined}
  408. showContextMenu
  409. widgetLimitReached={false}
  410. />
  411. );
  412. await waitFor(() => {
  413. expect(eventsMock).toHaveBeenCalledWith(
  414. '/organizations/org-slug/events/',
  415. expect.objectContaining({
  416. query: expect.objectContaining({
  417. per_page: 5,
  418. }),
  419. })
  420. );
  421. });
  422. });
  423. it('has sticky table headers', async function () {
  424. const tableWidget: Widget = {
  425. title: 'Table Widget',
  426. interval: '5m',
  427. displayType: DisplayType.TABLE,
  428. widgetType: WidgetType.DISCOVER,
  429. queries: [
  430. {
  431. conditions: '',
  432. fields: ['transaction', 'count()'],
  433. columns: ['transaction'],
  434. aggregates: ['count()'],
  435. name: 'Table',
  436. orderby: '',
  437. },
  438. ],
  439. };
  440. renderWithProviders(
  441. <WidgetCard
  442. api={api}
  443. widget={tableWidget}
  444. selection={selection}
  445. isEditingDashboard={false}
  446. onDelete={() => undefined}
  447. onEdit={() => undefined}
  448. onDuplicate={() => undefined}
  449. renderErrorMessage={() => undefined}
  450. showContextMenu
  451. widgetLimitReached={false}
  452. tableItemLimit={20}
  453. />
  454. );
  455. await waitFor(() => expect(eventsMock).toHaveBeenCalled());
  456. await waitFor(() =>
  457. expect(SimpleTableChart).toHaveBeenCalledWith(
  458. expect.objectContaining({stickyHeaders: true}),
  459. expect.anything()
  460. )
  461. );
  462. });
  463. it('calls release queries', function () {
  464. const widget: Widget = {
  465. title: 'Release Widget',
  466. interval: '5m',
  467. displayType: DisplayType.LINE,
  468. widgetType: WidgetType.RELEASE,
  469. queries: [],
  470. };
  471. renderWithProviders(
  472. <WidgetCard
  473. api={api}
  474. widget={widget}
  475. selection={selection}
  476. isEditingDashboard={false}
  477. onDelete={() => undefined}
  478. onEdit={() => undefined}
  479. onDuplicate={() => undefined}
  480. renderErrorMessage={() => undefined}
  481. showContextMenu
  482. widgetLimitReached={false}
  483. tableItemLimit={20}
  484. />
  485. );
  486. expect(ReleaseWidgetQueries).toHaveBeenCalledTimes(1);
  487. });
  488. it('opens the widget viewer modal when a widget has no id', async () => {
  489. const widget: Widget = {
  490. title: 'Widget',
  491. interval: '5m',
  492. displayType: DisplayType.LINE,
  493. widgetType: WidgetType.DISCOVER,
  494. queries: [],
  495. };
  496. renderWithProviders(
  497. <WidgetCard
  498. api={api}
  499. widget={widget}
  500. selection={selection}
  501. isEditingDashboard={false}
  502. onDelete={() => undefined}
  503. onEdit={() => undefined}
  504. onDuplicate={() => undefined}
  505. renderErrorMessage={() => undefined}
  506. showContextMenu
  507. widgetLimitReached={false}
  508. index="10"
  509. isPreview
  510. />
  511. );
  512. await userEvent.click(await screen.findByLabelText('Open Widget Viewer'));
  513. expect(router.push).toHaveBeenCalledWith(
  514. expect.objectContaining({pathname: '/mock-pathname/widget/10/'})
  515. );
  516. });
  517. it('renders stored data disclaimer', async function () {
  518. MockApiClient.addMockResponse({
  519. url: '/organizations/org-slug/events/',
  520. body: {
  521. meta: {title: 'string', isMetricsData: false},
  522. data: [{title: 'title'}],
  523. },
  524. });
  525. renderWithProviders(
  526. <WidgetCard
  527. api={api}
  528. organization={{
  529. ...organization,
  530. features: [...organization.features, 'dashboards-mep'],
  531. }}
  532. widget={{
  533. ...multipleQueryWidget,
  534. displayType: DisplayType.TABLE,
  535. queries: [{...multipleQueryWidget.queries[0]}],
  536. }}
  537. selection={selection}
  538. isEditingDashboard={false}
  539. onDelete={() => undefined}
  540. onEdit={() => undefined}
  541. onDuplicate={() => undefined}
  542. renderErrorMessage={() => undefined}
  543. showContextMenu
  544. widgetLimitReached={false}
  545. showStoredAlert
  546. />
  547. );
  548. // Badge in the widget header
  549. expect(await screen.findByText('Indexed')).toBeInTheDocument();
  550. expect(
  551. // Alert below the widget
  552. await screen.findByText(/we've automatically adjusted your results/i)
  553. ).toBeInTheDocument();
  554. });
  555. it('renders chart using axis and tooltip formatters from custom measurement meta', async function () {
  556. const spy = jest.spyOn(LineChart, 'LineChart');
  557. const eventsStatsMock = MockApiClient.addMockResponse({
  558. url: '/organizations/org-slug/events-stats/',
  559. body: {
  560. data: [
  561. [
  562. 1658262600,
  563. [
  564. {
  565. count: 24,
  566. },
  567. ],
  568. ],
  569. ],
  570. meta: {
  571. fields: {
  572. time: 'date',
  573. p95_measurements_custom: 'duration',
  574. },
  575. units: {
  576. time: null,
  577. p95_measurements_custom: 'millisecond',
  578. },
  579. isMetricsData: true,
  580. tips: {},
  581. },
  582. },
  583. });
  584. renderWithProviders(
  585. <WidgetCard
  586. api={api}
  587. organization={organization}
  588. widget={{
  589. title: '',
  590. interval: '5m',
  591. widgetType: WidgetType.DISCOVER,
  592. displayType: DisplayType.LINE,
  593. queries: [
  594. {
  595. conditions: '',
  596. name: '',
  597. fields: [],
  598. columns: [],
  599. aggregates: ['p95(measurements.custom)'],
  600. orderby: '',
  601. },
  602. ],
  603. }}
  604. selection={selection}
  605. isEditingDashboard={false}
  606. onDelete={() => undefined}
  607. onEdit={() => undefined}
  608. onDuplicate={() => undefined}
  609. renderErrorMessage={() => undefined}
  610. showContextMenu
  611. widgetLimitReached={false}
  612. />
  613. );
  614. await waitFor(function () {
  615. expect(eventsStatsMock).toHaveBeenCalled();
  616. });
  617. await waitFor(() => {
  618. const mockCall = spy.mock.calls?.at(-1)?.[0];
  619. expect(mockCall?.tooltip).toBeDefined();
  620. // @ts-expect-error
  621. expect(mockCall?.yAxis.axisLabel.formatter(24, 'p95(measurements.custom)')).toEqual(
  622. '24ms'
  623. );
  624. });
  625. });
  626. it('renders label in seconds when there is a transition from seconds to minutes in the y axis', async function () {
  627. const spy = jest.spyOn(LineChart, 'LineChart');
  628. const eventsStatsMock = MockApiClient.addMockResponse({
  629. url: '/organizations/org-slug/events-stats/',
  630. body: {
  631. data: [
  632. [
  633. 1658262600,
  634. [
  635. {
  636. count: 40 * SECOND,
  637. },
  638. ],
  639. ],
  640. [
  641. 1658262601,
  642. [
  643. {
  644. count: 50 * SECOND,
  645. },
  646. ],
  647. ],
  648. [
  649. 1658262602,
  650. [
  651. {
  652. count: MINUTE,
  653. },
  654. ],
  655. ],
  656. [
  657. 1658262603,
  658. [
  659. {
  660. count: 1.3 * MINUTE,
  661. },
  662. ],
  663. ],
  664. ],
  665. meta: {
  666. fields: {
  667. time: 'date',
  668. p50_transaction_duration: 'duration',
  669. },
  670. units: {
  671. time: null,
  672. p50_transaction_duration: 'millisecond',
  673. },
  674. isMetricsData: false,
  675. tips: {},
  676. },
  677. },
  678. });
  679. renderWithProviders(
  680. <WidgetCard
  681. api={api}
  682. organization={organization}
  683. widget={{
  684. title: '',
  685. interval: '5m',
  686. widgetType: WidgetType.DISCOVER,
  687. displayType: DisplayType.LINE,
  688. queries: [
  689. {
  690. conditions: '',
  691. name: '',
  692. fields: [],
  693. columns: [],
  694. aggregates: ['p50(transaction.duration)'],
  695. orderby: '',
  696. },
  697. ],
  698. }}
  699. selection={selection}
  700. isEditingDashboard={false}
  701. onDelete={() => undefined}
  702. onEdit={() => undefined}
  703. onDuplicate={() => undefined}
  704. renderErrorMessage={() => undefined}
  705. showContextMenu
  706. widgetLimitReached={false}
  707. />
  708. );
  709. await waitFor(function () {
  710. expect(eventsStatsMock).toHaveBeenCalled();
  711. });
  712. await waitFor(() => {
  713. const mockCall = spy.mock.calls?.at(-1)?.[0];
  714. expect(mockCall?.yAxis).toBeDefined();
  715. expect(
  716. // @ts-expect-error
  717. mockCall?.yAxis.axisLabel.formatter(60000, 'p50(transaction.duration)')
  718. ).toEqual('60s');
  719. // @ts-expect-error
  720. expect(mockCall?.yAxis?.minInterval).toEqual(SECOND);
  721. });
  722. });
  723. it('displays indexed badge in preview mode', async function () {
  724. renderWithProviders(
  725. <WidgetCard
  726. api={api}
  727. organization={{
  728. ...organization,
  729. features: [...organization.features, 'dashboards-mep'],
  730. }}
  731. widget={multipleQueryWidget}
  732. selection={selection}
  733. isEditingDashboard={false}
  734. onDelete={() => undefined}
  735. onEdit={() => undefined}
  736. onDuplicate={() => undefined}
  737. renderErrorMessage={() => undefined}
  738. showContextMenu
  739. widgetLimitReached={false}
  740. isPreview
  741. />
  742. );
  743. expect(await screen.findByText('Indexed')).toBeInTheDocument();
  744. });
  745. });