widgetViewerModal.spec.tsx 47 KB

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