index.spec.tsx 25 KB

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