index.spec.tsx 24 KB

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