index.spec.tsx 24 KB

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