index.spec.tsx 24 KB

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