widgetViewerModal.spec.tsx 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384
  1. import ReactEchartsCore from 'echarts-for-react/lib/core';
  2. import {MetricsTotalCountByReleaseIn24h} from 'sentry-fixture/metrics';
  3. import {ProjectFixture} from 'sentry-fixture/project';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import {resetMockDate, setMockDate} from 'sentry-test/utils';
  7. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  8. import WidgetViewerModal from 'sentry/components/modals/widgetViewerModal';
  9. import MemberListStore from 'sentry/stores/memberListStore';
  10. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import {space} from 'sentry/styles/space';
  13. import type {Series} from 'sentry/types/echarts';
  14. import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  15. import type {AggregationOutputType} from 'sentry/utils/discover/fields';
  16. import type {DashboardFilters, Widget, WidgetQuery} from 'sentry/views/dashboards/types';
  17. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  18. jest.mock('echarts-for-react/lib/core', () => {
  19. return jest.fn(({style}) => {
  20. return <div style={{...style, background: 'green'}}>echarts mock</div>;
  21. });
  22. });
  23. const stubEl = (props: {children?: React.ReactNode}) => <div>{props.children}</div>;
  24. let eventsMetaMock;
  25. const waitForMetaToHaveBeenCalled = async () => {
  26. await waitFor(() => {
  27. expect(eventsMetaMock).toHaveBeenCalled();
  28. });
  29. };
  30. async function renderModal({
  31. initialData: {organization, router},
  32. widget,
  33. seriesData,
  34. tableData,
  35. pageLinks,
  36. seriesResultsType,
  37. dashboardFilters,
  38. }: {
  39. initialData: any;
  40. widget: any;
  41. dashboardFilters?: DashboardFilters;
  42. pageLinks?: string;
  43. seriesData?: Series[];
  44. seriesResultsType?: Record<string, AggregationOutputType>;
  45. tableData?: TableDataWithTitle[];
  46. }) {
  47. const rendered = render(
  48. <div style={{padding: space(4)}}>
  49. <WidgetViewerModal
  50. Header={stubEl}
  51. Footer={stubEl as ModalRenderProps['Footer']}
  52. Body={stubEl as ModalRenderProps['Body']}
  53. CloseButton={stubEl}
  54. closeModal={() => undefined}
  55. organization={organization}
  56. widget={widget}
  57. onEdit={() => undefined}
  58. seriesData={seriesData}
  59. tableData={tableData}
  60. pageLinks={pageLinks}
  61. seriesResultsType={seriesResultsType}
  62. dashboardFilters={dashboardFilters}
  63. />
  64. </div>,
  65. {
  66. router,
  67. organization,
  68. }
  69. );
  70. // Need to wait since WidgetViewerModal will make a request to events-meta
  71. // for total events count on mount
  72. if (widget.widgetType === WidgetType.DISCOVER) {
  73. await waitForMetaToHaveBeenCalled();
  74. }
  75. // Component renders twice
  76. await act(tick);
  77. return rendered;
  78. }
  79. describe('Modals -> WidgetViewerModal', function () {
  80. let initialData, initialDataWithFlag;
  81. beforeEach(() => {
  82. initialData = initializeOrg({
  83. organization: {
  84. features: ['discover-query'],
  85. },
  86. router: {
  87. location: {query: {}},
  88. },
  89. projects: [ProjectFixture()],
  90. });
  91. initialDataWithFlag = {
  92. ...initialData,
  93. organization: {
  94. ...initialData.organization,
  95. features: [...initialData.organization.features],
  96. },
  97. };
  98. MockApiClient.addMockResponse({
  99. url: '/organizations/org-slug/projects/',
  100. body: [],
  101. });
  102. MockApiClient.addMockResponse({
  103. url: '/organizations/org-slug/releases/',
  104. body: [],
  105. });
  106. eventsMetaMock = MockApiClient.addMockResponse({
  107. url: '/organizations/org-slug/events-meta/',
  108. body: {count: 33323612},
  109. });
  110. PageFiltersStore.init();
  111. PageFiltersStore.onInitializeUrlState(
  112. {
  113. projects: [1, 2],
  114. environments: ['prod', 'dev'],
  115. datetime: {start: null, end: null, period: '24h', utc: null},
  116. },
  117. new Set()
  118. );
  119. });
  120. afterEach(() => {
  121. MockApiClient.clearMockResponses();
  122. ProjectsStore.reset();
  123. });
  124. describe('Discover Widgets', function () {
  125. describe('Area Chart Widget', function () {
  126. let mockQuery: WidgetQuery;
  127. let additionalMockQuery: WidgetQuery;
  128. let mockWidget: Widget;
  129. function mockEvents() {
  130. return MockApiClient.addMockResponse({
  131. url: '/organizations/org-slug/events/',
  132. body: {
  133. data: [
  134. {
  135. title: '/organizations/:orgId/dashboards/',
  136. id: '1',
  137. count: 1,
  138. },
  139. ],
  140. meta: {
  141. fields: {
  142. title: 'string',
  143. id: 'string',
  144. count: 1,
  145. },
  146. isMetricsData: false,
  147. },
  148. },
  149. });
  150. }
  151. beforeEach(function () {
  152. mockQuery = {
  153. conditions: 'title:/organizations/:orgId/performance/summary/',
  154. fields: ['count()'],
  155. aggregates: ['count()'],
  156. columns: [],
  157. name: 'Query Name',
  158. orderby: '',
  159. };
  160. additionalMockQuery = {
  161. conditions: '',
  162. fields: ['count()'],
  163. aggregates: ['count()'],
  164. columns: [],
  165. name: 'Another Query Name',
  166. orderby: '',
  167. };
  168. mockWidget = {
  169. id: '1',
  170. title: 'Test Widget',
  171. displayType: DisplayType.AREA,
  172. interval: '5m',
  173. queries: [mockQuery, additionalMockQuery],
  174. widgetType: WidgetType.DISCOVER,
  175. };
  176. (ReactEchartsCore as jest.Mock).mockClear();
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/events-stats/',
  179. body: {
  180. data: [
  181. [[1646100000], [{count: 1}]],
  182. [[1646120000], [{count: 1}]],
  183. ],
  184. start: 1646100000,
  185. end: 1646120000,
  186. isMetricsData: false,
  187. },
  188. });
  189. });
  190. it('renders Edit and Open buttons', async function () {
  191. mockEvents();
  192. await renderModal({initialData, widget: mockWidget});
  193. expect(await screen.findByText('Edit Widget')).toBeInTheDocument();
  194. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  195. });
  196. it('renders updated table columns and orderby', async function () {
  197. const eventsMock = mockEvents();
  198. await renderModal({initialData, widget: mockWidget});
  199. expect(await screen.findByText('title')).toBeInTheDocument();
  200. expect(
  201. await screen.findByText('/organizations/:orgId/dashboards/')
  202. ).toBeInTheDocument();
  203. expect(eventsMock).toHaveBeenCalledWith(
  204. '/organizations/org-slug/events/',
  205. expect.objectContaining({
  206. query: expect.objectContaining({sort: ['-count()']}),
  207. })
  208. );
  209. });
  210. it('applies the dashboard filters to the widget query when provided', async function () {
  211. const eventsMock = mockEvents();
  212. await renderModal({
  213. initialData,
  214. widget: mockWidget,
  215. dashboardFilters: {release: ['project-release@1.2.0']},
  216. });
  217. expect(await screen.findByText('title')).toBeInTheDocument();
  218. expect(
  219. await screen.findByText('/organizations/:orgId/dashboards/')
  220. ).toBeInTheDocument();
  221. expect(eventsMock).toHaveBeenCalledWith(
  222. '/organizations/org-slug/events/',
  223. expect.objectContaining({
  224. query: expect.objectContaining({
  225. query:
  226. // The release was injected into the discover query
  227. '(title:/organizations/:orgId/performance/summary/) release:"project-release@1.2.0" ',
  228. }),
  229. })
  230. );
  231. });
  232. it('renders area chart', async function () {
  233. mockEvents();
  234. await renderModal({initialData, widget: mockWidget});
  235. expect(await screen.findByText('echarts mock')).toBeInTheDocument();
  236. });
  237. it('renders description', async function () {
  238. mockEvents();
  239. await renderModal({
  240. initialData,
  241. widget: {...mockWidget, description: 'This is a description'},
  242. });
  243. expect(await screen.findByText('This is a description')).toBeInTheDocument();
  244. });
  245. it('redirects user to Discover when clicking Open in Discover', async function () {
  246. mockEvents();
  247. await renderModal({initialData, widget: mockWidget});
  248. await userEvent.click(await screen.findByText('Open in Discover'));
  249. expect(initialData.router.push).toHaveBeenCalledWith(
  250. '/organizations/org-slug/discover/results/?environment=prod&environment=dev&field=count%28%29&name=Test%20Widget&project=1&project=2&query=title%3A%2Forganizations%2F%3AorgId%2Fperformance%2Fsummary%2F&statsPeriod=24h&yAxis=count%28%29'
  251. );
  252. });
  253. it('zooms into the selected time range', async function () {
  254. mockEvents();
  255. await renderModal({initialData, widget: mockWidget});
  256. act(() => {
  257. // Simulate dataZoom event on chart
  258. (ReactEchartsCore as jest.Mock).mock.calls[0][0].onEvents.datazoom(undefined, {
  259. getModel: () => {
  260. return {
  261. _payload: {
  262. batch: [{startValue: 1646100000000, endValue: 1646120000000}],
  263. },
  264. };
  265. },
  266. });
  267. });
  268. await waitFor(() =>
  269. expect(initialData.router.push).toHaveBeenCalledWith(
  270. expect.objectContaining({
  271. query: {
  272. viewerEnd: '2022-03-01T07:33:20',
  273. viewerStart: '2022-03-01T02:00:00',
  274. },
  275. })
  276. )
  277. );
  278. });
  279. it('renders multiquery label and selector', async function () {
  280. mockEvents();
  281. await renderModal({initialData, widget: mockWidget});
  282. expect(
  283. await screen.findByText(
  284. 'This widget was built with multiple queries. Table data can only be displayed for one query at a time. To edit any of the queries, edit the widget.'
  285. )
  286. ).toBeInTheDocument();
  287. expect(screen.getByText('Query Name')).toBeInTheDocument();
  288. });
  289. it('updates selected query when selected in the query dropdown', async function () {
  290. mockEvents();
  291. const {rerender} = await renderModal({initialData, widget: mockWidget});
  292. await userEvent.click(await screen.findByText('Query Name'));
  293. await userEvent.click(screen.getByText('Another Query Name'));
  294. expect(initialData.router.replace).toHaveBeenCalledWith({
  295. query: {query: 1},
  296. });
  297. // Need to manually set the new router location and rerender to simulate the dropdown selection click
  298. initialData.router.location.query = {query: ['1']};
  299. rerender(
  300. <WidgetViewerModal
  301. Header={stubEl}
  302. Footer={stubEl as ModalRenderProps['Footer']}
  303. Body={stubEl as ModalRenderProps['Body']}
  304. CloseButton={stubEl}
  305. closeModal={() => undefined}
  306. organization={initialData.organization}
  307. widget={mockWidget}
  308. onEdit={() => undefined}
  309. />
  310. );
  311. await waitForMetaToHaveBeenCalled();
  312. expect(screen.getByText('Another Query Name')).toBeInTheDocument();
  313. });
  314. it('renders the correct discover query link when there are multiple queries in a widget', async function () {
  315. mockEvents();
  316. initialData.router.location.query = {query: ['1']};
  317. await renderModal({initialData, widget: mockWidget});
  318. expect(screen.getByRole('button', {name: 'Open in Discover'})).toHaveAttribute(
  319. 'href',
  320. '/organizations/org-slug/discover/results/?environment=prod&environment=dev&field=count%28%29&name=Test%20Widget&project=1&project=2&query=&statsPeriod=24h&yAxis=count%28%29'
  321. );
  322. });
  323. it('renders with first legend disabled by default', async function () {
  324. mockEvents();
  325. // Rerender with first legend disabled
  326. initialData.router.location.query = {legend: ['Query Name']};
  327. await renderModal({initialData, widget: mockWidget});
  328. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  329. expect.objectContaining({
  330. option: expect.objectContaining({
  331. legend: expect.objectContaining({
  332. selected: {'Query Name': false},
  333. }),
  334. }),
  335. }),
  336. {}
  337. );
  338. });
  339. it('renders total results in footer', async function () {
  340. mockEvents();
  341. await renderModal({initialData, widget: mockWidget});
  342. expect(await screen.findByText('33,323,612')).toBeInTheDocument();
  343. });
  344. it('renders highlighted query text and multiple queries in select dropdown', async function () {
  345. mockEvents();
  346. await renderModal({
  347. initialData,
  348. widget: {
  349. ...mockWidget,
  350. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  351. },
  352. });
  353. await userEvent.click(
  354. await screen.findByText('/organizations/:orgId/performance/summary/')
  355. );
  356. });
  357. it('renders widget chart minimap', async function () {
  358. initialData.organization.features.push('widget-viewer-modal-minimap');
  359. mockEvents();
  360. await renderModal({
  361. initialData,
  362. widget: {
  363. ...mockWidget,
  364. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  365. },
  366. });
  367. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  368. expect.objectContaining({
  369. option: expect.objectContaining({
  370. dataZoom: expect.arrayContaining([
  371. expect.objectContaining({
  372. realtime: false,
  373. showDetail: false,
  374. end: 100,
  375. start: 0,
  376. }),
  377. ]),
  378. }),
  379. }),
  380. {}
  381. );
  382. });
  383. it('zooming on minimap updates location query and updates echart start and end values', async function () {
  384. initialData.organization.features.push('widget-viewer-modal-minimap');
  385. mockEvents();
  386. await renderModal({
  387. initialData,
  388. widget: {
  389. ...mockWidget,
  390. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  391. },
  392. });
  393. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  394. act(() => {
  395. // Simulate dataZoom event on chart
  396. calls[calls.length - 1][0].onEvents.datazoom(
  397. {seriesStart: 1646100000000, seriesEnd: 1646120000000},
  398. {
  399. getModel: () => {
  400. return {
  401. _payload: {start: 30, end: 70},
  402. };
  403. },
  404. }
  405. );
  406. });
  407. await waitFor(() =>
  408. expect(initialData.router.push).toHaveBeenCalledWith(
  409. expect.objectContaining({
  410. query: {
  411. viewerEnd: '2022-03-01T05:53:20',
  412. viewerStart: '2022-03-01T03:40:00',
  413. },
  414. })
  415. )
  416. );
  417. });
  418. it('includes group by in widget viewer table', async function () {
  419. mockEvents();
  420. mockWidget.queries = [
  421. {
  422. conditions: 'title:/organizations/:orgId/performance/summary/',
  423. fields: ['count()'],
  424. aggregates: ['count()'],
  425. columns: ['transaction'],
  426. name: 'Query Name',
  427. orderby: '-count()',
  428. },
  429. ];
  430. await renderModal({initialData, widget: mockWidget});
  431. expect(await screen.findByText('transaction')).toBeInTheDocument();
  432. });
  433. it('includes order by in widget viewer table if not explicitly selected', async function () {
  434. mockEvents();
  435. mockWidget.queries = [
  436. {
  437. conditions: 'title:/organizations/:orgId/performance/summary/',
  438. fields: ['count()'],
  439. aggregates: ['count()'],
  440. columns: ['transaction'],
  441. name: 'Query Name',
  442. orderby: 'count_unique(user)',
  443. },
  444. ];
  445. await renderModal({initialData, widget: mockWidget});
  446. expect(await screen.findByText('count_unique(user)')).toBeInTheDocument();
  447. });
  448. it('includes a custom equation order by in widget viewer table if not explicitly selected', async function () {
  449. mockEvents();
  450. mockWidget.queries = [
  451. {
  452. conditions: 'title:/organizations/:orgId/performance/summary/',
  453. fields: ['count()'],
  454. aggregates: ['count()'],
  455. columns: ['transaction'],
  456. name: 'Query Name',
  457. orderby: '-equation|count_unique(user) + 1',
  458. },
  459. ];
  460. await renderModal({initialData, widget: mockWidget});
  461. expect(await screen.findByText('count_unique(user) + 1')).toBeInTheDocument();
  462. });
  463. it('renders widget chart with y axis formatter using provided seriesResultType', async function () {
  464. mockEvents();
  465. await renderModal({
  466. initialData: initialDataWithFlag,
  467. widget: mockWidget,
  468. seriesData: [],
  469. seriesResultsType: {'count()': 'duration', 'count_unique()': 'duration'},
  470. });
  471. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  472. const yAxisFormatter =
  473. calls[calls.length - 1][0].option.yAxis.axisLabel.formatter;
  474. expect(yAxisFormatter(123)).toEqual('123ms');
  475. });
  476. it('renders widget chart with default number y axis formatter when seriesResultType has multiple different types', async function () {
  477. mockEvents();
  478. await renderModal({
  479. initialData: initialDataWithFlag,
  480. widget: mockWidget,
  481. seriesData: [],
  482. seriesResultsType: {'count()': 'duration', 'count_unique()': 'size'},
  483. });
  484. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  485. const yAxisFormatter =
  486. calls[calls.length - 1][0].option.yAxis.axisLabel.formatter;
  487. expect(yAxisFormatter(123)).toEqual('123');
  488. });
  489. it('does not allow sorting by transaction name when widget is using metrics', async function () {
  490. const eventsMock = MockApiClient.addMockResponse({
  491. url: '/organizations/org-slug/events/',
  492. body: {
  493. data: [
  494. {
  495. title: '/organizations/:orgId/dashboards/',
  496. id: '1',
  497. count: 1,
  498. },
  499. ],
  500. meta: {
  501. fields: {
  502. title: 'string',
  503. id: 'string',
  504. count: 1,
  505. },
  506. isMetricsData: true,
  507. },
  508. },
  509. });
  510. await renderModal({
  511. initialData: initialDataWithFlag,
  512. widget: mockWidget,
  513. seriesData: [],
  514. seriesResultsType: {'count()': 'duration'},
  515. });
  516. expect(eventsMock).toHaveBeenCalledTimes(1);
  517. expect(screen.getByText('title')).toBeInTheDocument();
  518. await userEvent.click(screen.getByText('title'));
  519. expect(initialData.router.push).not.toHaveBeenCalledWith({
  520. query: {sort: ['-title']},
  521. });
  522. });
  523. it('renders transaction summary link', async function () {
  524. ProjectsStore.loadInitialData(initialData.projects);
  525. MockApiClient.addMockResponse({
  526. url: '/organizations/org-slug/events/',
  527. body: {
  528. data: [
  529. {
  530. title: '/organizations/:orgId/dashboards/',
  531. transaction: '/discover/homepage/',
  532. project: 'project-slug',
  533. id: '1',
  534. },
  535. ],
  536. meta: {
  537. fields: {
  538. title: 'string',
  539. transaction: 'string',
  540. project: 'string',
  541. id: 'string',
  542. },
  543. isMetricsData: true,
  544. },
  545. },
  546. });
  547. mockWidget.queries = [
  548. {
  549. conditions: 'title:/organizations/:orgId/performance/summary/',
  550. fields: [''],
  551. aggregates: [''],
  552. columns: ['transaction'],
  553. name: 'Query Name',
  554. orderby: '',
  555. },
  556. ];
  557. await renderModal({
  558. initialData: initialDataWithFlag,
  559. widget: mockWidget,
  560. seriesData: [],
  561. seriesResultsType: {'count()': 'duration'},
  562. });
  563. const link = await screen.findByTestId('widget-viewer-transaction-link');
  564. expect(link).toHaveAttribute(
  565. 'href',
  566. expect.stringMatching(
  567. RegExp(
  568. '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
  569. )
  570. )
  571. );
  572. });
  573. });
  574. describe('TopN Chart Widget', function () {
  575. let mockQuery, mockWidget;
  576. function mockEventsStats() {
  577. return MockApiClient.addMockResponse({
  578. url: '/organizations/org-slug/events-stats/',
  579. body: {
  580. data: [
  581. [[1646100000], [{count: 1}]],
  582. [[1646120000], [{count: 1}]],
  583. ],
  584. start: 1646100000,
  585. end: 1646120000,
  586. isMetricsData: false,
  587. },
  588. });
  589. }
  590. const eventsMockData = [
  591. {
  592. 'error.type': ['Test Error 1a', 'Test Error 1b', 'Test Error 1c'],
  593. count: 10,
  594. },
  595. {
  596. 'error.type': ['Test Error 2'],
  597. count: 6,
  598. },
  599. {
  600. 'error.type': ['Test Error 3'],
  601. count: 5,
  602. },
  603. {
  604. 'error.type': ['Test Error 4'],
  605. count: 4,
  606. },
  607. {
  608. 'error.type': ['Test Error 5'],
  609. count: 3,
  610. },
  611. {
  612. 'error.type': ['Test Error 6'],
  613. count: 2,
  614. },
  615. ];
  616. function mockEvents() {
  617. return MockApiClient.addMockResponse({
  618. url: '/organizations/org-slug/events/',
  619. match: [MockApiClient.matchQuery({cursor: undefined})],
  620. headers: {
  621. Link:
  622. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  623. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  624. },
  625. body: {
  626. data: eventsMockData,
  627. meta: {
  628. fields: {
  629. 'error.type': 'array',
  630. count: 'integer',
  631. },
  632. },
  633. },
  634. });
  635. }
  636. beforeEach(function () {
  637. mockQuery = {
  638. conditions: 'title:/organizations/:orgId/performance/summary/',
  639. fields: ['error.type', 'count()'],
  640. aggregates: ['count()'],
  641. columns: ['error.type'],
  642. id: '1',
  643. name: 'Query Name',
  644. orderby: '',
  645. };
  646. mockWidget = {
  647. title: 'Test Widget',
  648. displayType: DisplayType.TOP_N,
  649. interval: '5m',
  650. queries: [mockQuery],
  651. widgetType: WidgetType.DISCOVER,
  652. };
  653. MockApiClient.addMockResponse({
  654. url: '/organizations/org-slug/events/',
  655. match: [MockApiClient.matchQuery({cursor: '0:10:0'})],
  656. headers: {
  657. Link:
  658. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  659. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:20:0>; rel="next"; results="true"; cursor="0:20:0"',
  660. },
  661. body: {
  662. data: [
  663. {
  664. 'error.type': ['Next Page Test Error'],
  665. count: 1,
  666. },
  667. ],
  668. meta: {
  669. fields: {
  670. 'error.type': 'array',
  671. count: 'integer',
  672. },
  673. },
  674. },
  675. });
  676. });
  677. it('sorts table when a sortable column header is clicked', async function () {
  678. const eventsStatsMock = mockEventsStats();
  679. const eventsMock = mockEvents();
  680. const {rerender} = await renderModal({initialData, widget: mockWidget});
  681. await userEvent.click(await screen.findByText('count()'));
  682. expect(initialData.router.push).toHaveBeenCalledWith({
  683. query: {sort: ['-count()']},
  684. });
  685. // Need to manually set the new router location and rerender to simulate the sortable column click
  686. initialData.router.location.query = {sort: ['-count()']};
  687. rerender(
  688. <WidgetViewerModal
  689. Header={stubEl}
  690. Footer={stubEl as ModalRenderProps['Footer']}
  691. Body={stubEl as ModalRenderProps['Body']}
  692. CloseButton={stubEl}
  693. closeModal={() => undefined}
  694. organization={initialData.organization}
  695. widget={mockWidget}
  696. onEdit={() => undefined}
  697. />
  698. );
  699. await waitForMetaToHaveBeenCalled();
  700. expect(eventsMock).toHaveBeenCalledWith(
  701. '/organizations/org-slug/events/',
  702. expect.objectContaining({
  703. query: expect.objectContaining({sort: ['-count()']}),
  704. })
  705. );
  706. expect(eventsStatsMock).toHaveBeenCalledWith(
  707. '/organizations/org-slug/events-stats/',
  708. expect.objectContaining({
  709. query: expect.objectContaining({orderby: '-count()'}),
  710. })
  711. );
  712. });
  713. it('renders pagination buttons', async function () {
  714. mockEventsStats();
  715. mockEvents();
  716. await renderModal({initialData, widget: mockWidget});
  717. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  718. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  719. });
  720. it('does not render pagination buttons', async function () {
  721. mockEventsStats();
  722. mockEvents();
  723. MockApiClient.addMockResponse({
  724. url: '/organizations/org-slug/events/',
  725. headers: {
  726. Link:
  727. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  728. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:20:0>; rel="next"; results="false"; cursor="0:20:0"',
  729. },
  730. body: {
  731. data: [
  732. {
  733. 'error.type': ['No Pagination'],
  734. count: 1,
  735. },
  736. ],
  737. meta: {
  738. 'error.type': 'array',
  739. count: 'integer',
  740. },
  741. },
  742. });
  743. await renderModal({initialData, widget: mockWidget});
  744. expect(screen.queryByRole('button', {name: 'Previous'})).not.toBeInTheDocument();
  745. expect(screen.queryByRole('button', {name: 'Next'})).not.toBeInTheDocument();
  746. });
  747. it('paginates to the next page', async function () {
  748. mockEventsStats();
  749. mockEvents();
  750. const {rerender} = await renderModal({initialData, widget: mockWidget});
  751. expect(await screen.findByText('Test Error 1c')).toBeInTheDocument();
  752. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  753. expect(initialData.router.replace).toHaveBeenCalledWith(
  754. expect.objectContaining({
  755. query: {cursor: '0:10:0'},
  756. })
  757. );
  758. // Need to manually set the new router location and rerender to simulate the next page click
  759. initialData.router.location.query = {cursor: ['0:10:0']};
  760. rerender(
  761. <WidgetViewerModal
  762. Header={stubEl}
  763. Footer={stubEl as ModalRenderProps['Footer']}
  764. Body={stubEl as ModalRenderProps['Body']}
  765. CloseButton={stubEl}
  766. closeModal={() => undefined}
  767. organization={initialData.organization}
  768. widget={mockWidget}
  769. onEdit={() => undefined}
  770. />
  771. );
  772. await waitForMetaToHaveBeenCalled();
  773. expect(await screen.findByText('Next Page Test Error')).toBeInTheDocument();
  774. });
  775. it('uses provided seriesData and does not make an events-stats requests', async function () {
  776. const eventsStatsMock = mockEventsStats();
  777. mockEvents();
  778. await renderModal({initialData, widget: mockWidget, seriesData: []});
  779. expect(eventsStatsMock).not.toHaveBeenCalled();
  780. });
  781. it('makes events-stats requests when table is sorted', async function () {
  782. const eventsStatsMock = mockEventsStats();
  783. mockEvents();
  784. await renderModal({
  785. initialData,
  786. widget: mockWidget,
  787. seriesData: [],
  788. });
  789. expect(eventsStatsMock).not.toHaveBeenCalled();
  790. await userEvent.click(screen.getByText('count()'));
  791. await waitForMetaToHaveBeenCalled();
  792. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  793. });
  794. it('renders widget chart minimap', async function () {
  795. mockEventsStats();
  796. mockEvents();
  797. initialData.organization.features.push('widget-viewer-modal-minimap');
  798. await renderModal({initialData, widget: mockWidget});
  799. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  800. expect.objectContaining({
  801. option: expect.objectContaining({
  802. dataZoom: expect.arrayContaining([
  803. expect.objectContaining({
  804. realtime: false,
  805. showDetail: false,
  806. end: 100,
  807. start: 0,
  808. }),
  809. ]),
  810. }),
  811. }),
  812. {}
  813. );
  814. });
  815. it('zooming on minimap updates location query and updates echart start and end values', async function () {
  816. mockEventsStats();
  817. mockEvents();
  818. initialData.organization.features.push('widget-viewer-modal-minimap');
  819. await renderModal({initialData, widget: mockWidget});
  820. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  821. act(() => {
  822. // Simulate dataZoom event on chart
  823. calls[calls.length - 1][0].onEvents.datazoom(
  824. {seriesStart: 1646100000000, seriesEnd: 1646120000000},
  825. {
  826. getModel: () => {
  827. return {
  828. _payload: {start: 30, end: 70},
  829. };
  830. },
  831. }
  832. );
  833. });
  834. expect(initialData.router.push).toHaveBeenCalledWith(
  835. expect.objectContaining({
  836. query: {
  837. viewerEnd: '2022-03-01T05:53:20',
  838. viewerStart: '2022-03-01T03:40:00',
  839. },
  840. })
  841. );
  842. await waitFor(() => {
  843. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  844. expect.objectContaining({
  845. option: expect.objectContaining({
  846. dataZoom: expect.arrayContaining([
  847. expect.objectContaining({
  848. realtime: false,
  849. showDetail: false,
  850. endValue: 1646114000000,
  851. startValue: 1646106000000,
  852. }),
  853. ]),
  854. }),
  855. }),
  856. {}
  857. );
  858. });
  859. });
  860. });
  861. describe('Table Widget', function () {
  862. const mockQuery = {
  863. conditions: 'title:/organizations/:orgId/performance/summary/',
  864. fields: ['title', 'count()'],
  865. aggregates: ['count()'],
  866. columns: ['title'],
  867. id: '1',
  868. name: 'Query Name',
  869. orderby: '',
  870. };
  871. const mockWidget = {
  872. title: 'Test Widget',
  873. displayType: DisplayType.TABLE,
  874. interval: '5m',
  875. queries: [mockQuery],
  876. widgetType: WidgetType.DISCOVER,
  877. };
  878. function mockEvents() {
  879. return MockApiClient.addMockResponse({
  880. url: '/organizations/org-slug/events/',
  881. body: {
  882. data: [
  883. {
  884. title: '/organizations/:orgId/dashboards/',
  885. id: '1',
  886. count: 1,
  887. },
  888. ],
  889. meta: {
  890. fields: {
  891. title: 'string',
  892. id: 'string',
  893. count: 1,
  894. },
  895. isMetricsData: false,
  896. },
  897. },
  898. });
  899. }
  900. it('makes events requests when table is paginated', async function () {
  901. const eventsMock = mockEvents();
  902. await renderModal({
  903. initialData,
  904. widget: mockWidget,
  905. tableData: [],
  906. pageLinks:
  907. '<https://sentry.io>; rel="previous"; results="false"; cursor="0:0:1", <https://sentry.io>; rel="next"; results="true"; cursor="0:20:0"',
  908. });
  909. await act(tick);
  910. expect(eventsMock).not.toHaveBeenCalled();
  911. await userEvent.click(await screen.findByLabelText('Next'));
  912. await waitFor(() => {
  913. expect(eventsMock).toHaveBeenCalled();
  914. });
  915. });
  916. it('displays table data with units correctly', async function () {
  917. const eventsMock = MockApiClient.addMockResponse({
  918. url: '/organizations/org-slug/events/',
  919. match: [MockApiClient.matchQuery({cursor: undefined})],
  920. headers: {
  921. Link:
  922. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  923. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  924. },
  925. body: {
  926. data: [
  927. {
  928. 'p75(measurements.custom.minute)': 94.87035966318831,
  929. 'p95(measurements.custom.ratio)': 0.9881980140455187,
  930. 'p75(measurements.custom.kibibyte)': 217.87035966318834,
  931. },
  932. ],
  933. meta: {
  934. fields: {
  935. 'p75(measurements.custom.minute)': 'duration',
  936. 'p95(measurements.custom.ratio)': 'percentage',
  937. 'p75(measurements.custom.kibibyte)': 'size',
  938. },
  939. units: {
  940. 'p75(measurements.custom.minute)': 'minute',
  941. 'p95(measurements.custom.ratio)': null,
  942. 'p75(measurements.custom.kibibyte)': 'kibibyte',
  943. },
  944. isMetricsData: true,
  945. tips: {},
  946. },
  947. },
  948. });
  949. await renderModal({
  950. initialData: initialDataWithFlag,
  951. widget: {
  952. title: 'Custom Widget',
  953. displayType: 'table',
  954. queries: [
  955. {
  956. fields: [
  957. 'p75(measurements.custom.kibibyte)',
  958. 'p75(measurements.custom.minute)',
  959. 'p95(measurements.custom.ratio)',
  960. ],
  961. aggregates: [
  962. 'p75(measurements.custom.kibibyte)',
  963. 'p75(measurements.custom.minute)',
  964. 'p95(measurements.custom.ratio)',
  965. ],
  966. columns: [],
  967. orderby: '-p75(measurements.custom.kibibyte)',
  968. },
  969. ],
  970. widgetType: 'discover',
  971. },
  972. });
  973. await waitFor(() => {
  974. expect(eventsMock).toHaveBeenCalled();
  975. });
  976. expect(screen.getByText('217.9 KiB')).toBeInTheDocument();
  977. expect(screen.getByText('1.58hr')).toBeInTheDocument();
  978. expect(screen.getByText('98.82%')).toBeInTheDocument();
  979. });
  980. });
  981. });
  982. describe('Issue Table Widget', function () {
  983. let issuesMock;
  984. const mockQuery = {
  985. conditions: 'is:unresolved',
  986. fields: ['events', 'status', 'title'],
  987. columns: ['events', 'status', 'title'],
  988. aggregates: [],
  989. id: '1',
  990. name: 'Query Name',
  991. orderby: '',
  992. };
  993. const mockWidget = {
  994. id: '1',
  995. title: 'Issue Widget',
  996. displayType: DisplayType.TABLE,
  997. interval: '5m',
  998. queries: [mockQuery],
  999. widgetType: WidgetType.ISSUE,
  1000. };
  1001. beforeEach(function () {
  1002. MemberListStore.loadInitialData([]);
  1003. MockApiClient.addMockResponse({
  1004. url: '/organizations/org-slug/issues/',
  1005. method: 'GET',
  1006. match: [
  1007. MockApiClient.matchData({
  1008. cursor: '0:10:0',
  1009. }),
  1010. ],
  1011. headers: {
  1012. Link:
  1013. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1014. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:20:0>; rel="next"; results="true"; cursor="0:20:0"',
  1015. },
  1016. body: [
  1017. {
  1018. id: '2',
  1019. title: 'Another Error: Failed',
  1020. project: {
  1021. id: '3',
  1022. },
  1023. status: 'unresolved',
  1024. lifetime: {count: 5},
  1025. count: 3,
  1026. userCount: 1,
  1027. },
  1028. ],
  1029. });
  1030. issuesMock = MockApiClient.addMockResponse({
  1031. url: '/organizations/org-slug/issues/',
  1032. method: 'GET',
  1033. match: [
  1034. MockApiClient.matchData({
  1035. cursor: undefined,
  1036. }),
  1037. ],
  1038. headers: {
  1039. Link:
  1040. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1041. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  1042. },
  1043. body: [
  1044. {
  1045. id: '1',
  1046. title: 'Error: Failed',
  1047. project: {
  1048. id: '3',
  1049. },
  1050. status: 'unresolved',
  1051. lifetime: {count: 10},
  1052. count: 6,
  1053. userCount: 3,
  1054. },
  1055. ],
  1056. });
  1057. });
  1058. it('renders widget title', async function () {
  1059. await renderModal({initialData, widget: mockWidget});
  1060. expect(await screen.findByText('Issue Widget')).toBeInTheDocument();
  1061. });
  1062. it('renders Edit and Open buttons', async function () {
  1063. await renderModal({initialData, widget: mockWidget});
  1064. expect(await screen.findByText('Edit Widget')).toBeInTheDocument();
  1065. expect(screen.getByText('Open in Issues')).toBeInTheDocument();
  1066. });
  1067. it('renders events, status, async and title table columns', async function () {
  1068. await renderModal({initialData, widget: mockWidget});
  1069. expect(await screen.findByText('Error: Failed')).toBeInTheDocument();
  1070. expect(screen.getByText('title')).toBeInTheDocument();
  1071. expect(screen.getByText('events')).toBeInTheDocument();
  1072. expect(screen.getByText('6')).toBeInTheDocument();
  1073. expect(screen.getByText('status')).toBeInTheDocument();
  1074. expect(screen.getByText('unresolved')).toBeInTheDocument();
  1075. });
  1076. it('renders Issue table widget viewer', async function () {
  1077. await renderModal({initialData, widget: mockWidget});
  1078. await screen.findByText('Error: Failed');
  1079. });
  1080. it('redirects user to Issues when clicking Open in Issues', async function () {
  1081. await renderModal({initialData, widget: mockWidget});
  1082. await userEvent.click(screen.getByText('Open in Issues'));
  1083. expect(initialData.router.push).toHaveBeenCalledWith(
  1084. '/organizations/org-slug/issues/?environment=prod&environment=dev&project=1&project=2&query=is%3Aunresolved&sort=&statsPeriod=24h'
  1085. );
  1086. });
  1087. it('sorts table when a sortable column header is clicked', async function () {
  1088. const {rerender} = await renderModal({initialData, widget: mockWidget});
  1089. await userEvent.click(screen.getByText('events'));
  1090. expect(initialData.router.push).toHaveBeenCalledWith({
  1091. query: {sort: 'freq'},
  1092. });
  1093. // Need to manually set the new router location and rerender to simulate the sortable column click
  1094. initialData.router.location.query = {sort: ['freq']};
  1095. rerender(
  1096. <WidgetViewerModal
  1097. Header={stubEl}
  1098. Footer={stubEl as ModalRenderProps['Footer']}
  1099. Body={stubEl as ModalRenderProps['Body']}
  1100. CloseButton={stubEl}
  1101. closeModal={() => undefined}
  1102. organization={initialData.organization}
  1103. widget={mockWidget}
  1104. onEdit={() => undefined}
  1105. />
  1106. );
  1107. await waitFor(() =>
  1108. expect(issuesMock).toHaveBeenCalledWith(
  1109. '/organizations/org-slug/issues/',
  1110. expect.objectContaining({
  1111. data: {
  1112. cursor: undefined,
  1113. environment: ['prod', 'dev'],
  1114. expand: ['owners'],
  1115. limit: 20,
  1116. project: [1, 2],
  1117. query: 'is:unresolved',
  1118. sort: 'date',
  1119. statsPeriod: '24h',
  1120. },
  1121. })
  1122. )
  1123. );
  1124. });
  1125. it('renders pagination buttons', async function () {
  1126. await renderModal({initialData, widget: mockWidget});
  1127. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1128. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1129. });
  1130. it('paginates to the next page', async function () {
  1131. const {rerender} = await renderModal({initialData, widget: mockWidget});
  1132. expect(await screen.findByText('Error: Failed')).toBeInTheDocument();
  1133. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  1134. expect(issuesMock).toHaveBeenCalledTimes(1);
  1135. expect(initialData.router.replace).toHaveBeenCalledWith(
  1136. expect.objectContaining({
  1137. query: {cursor: '0:10:0', page: 1},
  1138. })
  1139. );
  1140. // Need to manually set the new router location and rerender to simulate the next page click
  1141. initialData.router.location.query = {cursor: ['0:10:0']};
  1142. rerender(
  1143. <WidgetViewerModal
  1144. Header={stubEl}
  1145. Footer={stubEl as ModalRenderProps['Footer']}
  1146. Body={stubEl as ModalRenderProps['Body']}
  1147. CloseButton={stubEl}
  1148. closeModal={() => undefined}
  1149. organization={initialData.organization}
  1150. widget={mockWidget}
  1151. onEdit={() => undefined}
  1152. />
  1153. );
  1154. expect(await screen.findByText('Another Error: Failed')).toBeInTheDocument();
  1155. });
  1156. it('displays with correct table column widths', async function () {
  1157. initialData.router.location.query = {width: ['-1', '-1', '575']};
  1158. await renderModal({initialData, widget: mockWidget});
  1159. expect(await screen.findByTestId('grid-editable')).toHaveStyle({
  1160. 'grid-template-columns':
  1161. ' minmax(90px, auto) minmax(90px, auto) minmax(575px, auto)',
  1162. });
  1163. });
  1164. it('uses provided tableData and does not make an issues requests', async function () {
  1165. await renderModal({initialData, widget: mockWidget, tableData: []});
  1166. expect(issuesMock).not.toHaveBeenCalled();
  1167. });
  1168. it('makes issues requests when table is sorted', async function () {
  1169. await renderModal({
  1170. initialData,
  1171. widget: mockWidget,
  1172. tableData: [],
  1173. });
  1174. expect(issuesMock).not.toHaveBeenCalled();
  1175. await userEvent.click(screen.getByText('events'));
  1176. await waitFor(() => {
  1177. expect(issuesMock).toHaveBeenCalled();
  1178. });
  1179. });
  1180. });
  1181. describe('Release Health Widgets', function () {
  1182. let metricsMock;
  1183. const mockQuery = {
  1184. conditions: '',
  1185. fields: [`sum(session)`],
  1186. columns: [],
  1187. aggregates: [],
  1188. id: '1',
  1189. name: 'Query Name',
  1190. orderby: '',
  1191. };
  1192. const mockWidget = {
  1193. id: '1',
  1194. title: 'Release Widget',
  1195. displayType: DisplayType.LINE,
  1196. interval: '5m',
  1197. queries: [mockQuery],
  1198. widgetType: WidgetType.RELEASE,
  1199. };
  1200. beforeEach(function () {
  1201. setMockDate(new Date('2022-08-02'));
  1202. metricsMock = MockApiClient.addMockResponse({
  1203. url: '/organizations/org-slug/metrics/data/',
  1204. body: MetricsTotalCountByReleaseIn24h(),
  1205. headers: {
  1206. link:
  1207. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1208. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  1209. },
  1210. });
  1211. });
  1212. afterEach(() => {
  1213. resetMockDate();
  1214. });
  1215. it('does a sessions query', async function () {
  1216. await renderModal({initialData, widget: mockWidget});
  1217. await waitFor(() => {
  1218. expect(metricsMock).toHaveBeenCalled();
  1219. });
  1220. });
  1221. it('renders widget title', async function () {
  1222. await renderModal({initialData, widget: mockWidget});
  1223. expect(await screen.findByText('Release Widget')).toBeInTheDocument();
  1224. });
  1225. it('renders Edit and Open in Releases buttons', async function () {
  1226. await renderModal({initialData, widget: mockWidget});
  1227. expect(await screen.findByText('Edit Widget')).toBeInTheDocument();
  1228. expect(screen.getByText('Open in Releases')).toBeInTheDocument();
  1229. });
  1230. it('Open in Releases button redirects browser', async function () {
  1231. await renderModal({initialData, widget: mockWidget});
  1232. await userEvent.click(screen.getByText('Open in Releases'), {delay: null});
  1233. expect(initialData.router.push).toHaveBeenCalledWith(
  1234. '/organizations/org-slug/releases/?environment=prod&environment=dev&project=1&project=2&statsPeriod=24h'
  1235. );
  1236. });
  1237. it('renders table header and body', async function () {
  1238. await renderModal({initialData, widget: mockWidget});
  1239. expect(await screen.findByText('release')).toBeInTheDocument();
  1240. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1241. expect(screen.getByText('sum(session)')).toBeInTheDocument();
  1242. expect(screen.getByText('6.3k')).toBeInTheDocument();
  1243. });
  1244. it('renders Release widget viewer', async function () {
  1245. await renderModal({initialData, widget: mockWidget});
  1246. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1247. });
  1248. it('renders pagination buttons', async function () {
  1249. await renderModal({
  1250. initialData,
  1251. widget: mockWidget,
  1252. });
  1253. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1254. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1255. });
  1256. it('does not render pagination buttons when sorting by release', async function () {
  1257. // TODO(scttcper): We shouldn't need to wrap render with act, it seems to double render ReleaseWidgetQueries
  1258. await act(() =>
  1259. renderModal({
  1260. initialData,
  1261. widget: {...mockWidget, queries: [{...mockQuery, orderby: 'release'}]},
  1262. // in react 17 act requires that nothing is returned
  1263. }).then(() => void 0)
  1264. );
  1265. expect(screen.queryByRole('button', {name: 'Previous'})).not.toBeInTheDocument();
  1266. expect(screen.queryByRole('button', {name: 'Next'})).not.toBeInTheDocument();
  1267. });
  1268. it('makes a new sessions request after sorting by a table column', async function () {
  1269. const {rerender} = await renderModal({
  1270. initialData,
  1271. widget: mockWidget,
  1272. tableData: [],
  1273. seriesData: [],
  1274. });
  1275. expect(metricsMock).toHaveBeenCalledTimes(1);
  1276. await userEvent.click(await screen.findByText(`sum(session)`), {delay: null});
  1277. expect(initialData.router.push).toHaveBeenCalledWith({
  1278. query: {sort: '-sum(session)'},
  1279. });
  1280. // Need to manually set the new router location and rerender to simulate the sortable column click
  1281. initialData.router.location.query = {sort: '-sum(session)'};
  1282. rerender(
  1283. <WidgetViewerModal
  1284. Header={stubEl}
  1285. Footer={stubEl as ModalRenderProps['Footer']}
  1286. Body={stubEl as ModalRenderProps['Body']}
  1287. CloseButton={stubEl}
  1288. closeModal={() => undefined}
  1289. organization={initialData.organization}
  1290. widget={mockWidget}
  1291. onEdit={() => undefined}
  1292. seriesData={[]}
  1293. tableData={[]}
  1294. />
  1295. );
  1296. await waitFor(() => {
  1297. expect(metricsMock).toHaveBeenCalledTimes(2);
  1298. });
  1299. });
  1300. });
  1301. });