index.spec.tsx 24 KB

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