widgetViewerModal.spec.tsx 50 KB

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