widgetCard.spec.tsx 25 KB

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