index.spec.tsx 24 KB

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