widgetViewerModal.spec.tsx 47 KB

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