widgetViewerModal.spec.tsx 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380
  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 {ModalRenderProps} from 'sentry/actionCreators/modal';
  7. import WidgetViewerModal from 'sentry/components/modals/widgetViewerModal';
  8. import MemberListStore from 'sentry/stores/memberListStore';
  9. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  10. import ProjectsStore from 'sentry/stores/projectsStore';
  11. import {space} from 'sentry/styles/space';
  12. import {Series} from 'sentry/types/echarts';
  13. import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  14. import {AggregationOutputType} from 'sentry/utils/discover/fields';
  15. import {
  16. DashboardFilters,
  17. DisplayType,
  18. Widget,
  19. WidgetQuery,
  20. WidgetType,
  21. } from 'sentry/views/dashboards/types';
  22. jest.mock('echarts-for-react/lib/core', () => {
  23. return jest.fn(({style}) => {
  24. return <div style={{...style, background: 'green'}}>echarts mock</div>;
  25. });
  26. });
  27. const stubEl = (props: {children?: React.ReactNode}) => <div>{props.children}</div>;
  28. let eventsMetaMock;
  29. const waitForMetaToHaveBeenCalled = async () => {
  30. await waitFor(() => {
  31. expect(eventsMetaMock).toHaveBeenCalled();
  32. });
  33. };
  34. async function renderModal({
  35. initialData: {organization, routerContext},
  36. widget,
  37. seriesData,
  38. tableData,
  39. pageLinks,
  40. seriesResultsType,
  41. dashboardFilters,
  42. }: {
  43. initialData: any;
  44. widget: any;
  45. dashboardFilters?: DashboardFilters;
  46. pageLinks?: string;
  47. seriesData?: Series[];
  48. seriesResultsType?: Record<string, AggregationOutputType>;
  49. tableData?: TableDataWithTitle[];
  50. }) {
  51. const rendered = render(
  52. <div style={{padding: space(4)}}>
  53. <WidgetViewerModal
  54. Header={stubEl}
  55. Footer={stubEl as ModalRenderProps['Footer']}
  56. Body={stubEl as ModalRenderProps['Body']}
  57. CloseButton={stubEl}
  58. closeModal={() => undefined}
  59. organization={organization}
  60. widget={widget}
  61. onEdit={() => undefined}
  62. seriesData={seriesData}
  63. tableData={tableData}
  64. pageLinks={pageLinks}
  65. seriesResultsType={seriesResultsType}
  66. dashboardFilters={dashboardFilters}
  67. />
  68. </div>,
  69. {
  70. context: routerContext,
  71. organization,
  72. }
  73. );
  74. // Need to wait since WidgetViewerModal will make a request to events-meta
  75. // for total events count on mount
  76. if (widget.widgetType === WidgetType.DISCOVER) {
  77. await waitForMetaToHaveBeenCalled();
  78. }
  79. return rendered;
  80. }
  81. describe('Modals -> WidgetViewerModal', function () {
  82. let initialData, initialDataWithFlag;
  83. beforeEach(() => {
  84. initialData = initializeOrg({
  85. organization: {
  86. features: ['discover-query'],
  87. },
  88. router: {
  89. location: {query: {}},
  90. },
  91. projects: [ProjectFixture()],
  92. });
  93. initialDataWithFlag = {
  94. ...initialData,
  95. organization: {
  96. ...initialData.organization,
  97. features: [...initialData.organization.features],
  98. },
  99. };
  100. MockApiClient.addMockResponse({
  101. url: '/organizations/org-slug/projects/',
  102. body: [],
  103. });
  104. MockApiClient.addMockResponse({
  105. url: '/organizations/org-slug/releases/',
  106. body: [],
  107. });
  108. eventsMetaMock = MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/events-meta/',
  110. body: {count: 33323612},
  111. });
  112. PageFiltersStore.init();
  113. PageFiltersStore.onInitializeUrlState(
  114. {
  115. projects: [1, 2],
  116. environments: ['prod', 'dev'],
  117. datetime: {start: null, end: null, period: '24h', utc: null},
  118. },
  119. new Set()
  120. );
  121. });
  122. afterEach(() => {
  123. MockApiClient.clearMockResponses();
  124. ProjectsStore.reset();
  125. });
  126. describe('Discover Widgets', function () {
  127. describe('Area Chart Widget', function () {
  128. let mockQuery: WidgetQuery;
  129. let additionalMockQuery: WidgetQuery;
  130. let mockWidget: Widget;
  131. function mockEvents() {
  132. return MockApiClient.addMockResponse({
  133. url: '/organizations/org-slug/events/',
  134. body: {
  135. data: [
  136. {
  137. title: '/organizations/:orgId/dashboards/',
  138. id: '1',
  139. count: 1,
  140. },
  141. ],
  142. meta: {
  143. fields: {
  144. title: 'string',
  145. id: 'string',
  146. count: 1,
  147. },
  148. isMetricsData: false,
  149. },
  150. },
  151. });
  152. }
  153. beforeEach(function () {
  154. mockQuery = {
  155. conditions: 'title:/organizations/:orgId/performance/summary/',
  156. fields: ['count()'],
  157. aggregates: ['count()'],
  158. columns: [],
  159. name: 'Query Name',
  160. orderby: '',
  161. };
  162. additionalMockQuery = {
  163. conditions: '',
  164. fields: ['count()'],
  165. aggregates: ['count()'],
  166. columns: [],
  167. name: 'Another Query Name',
  168. orderby: '',
  169. };
  170. mockWidget = {
  171. id: '1',
  172. title: 'Test Widget',
  173. displayType: DisplayType.AREA,
  174. interval: '5m',
  175. queries: [mockQuery, additionalMockQuery],
  176. widgetType: WidgetType.DISCOVER,
  177. };
  178. (ReactEchartsCore as jest.Mock).mockClear();
  179. MockApiClient.addMockResponse({
  180. url: '/organizations/org-slug/events-stats/',
  181. body: {
  182. data: [
  183. [[1646100000], [{count: 1}]],
  184. [[1646120000], [{count: 1}]],
  185. ],
  186. start: 1646100000,
  187. end: 1646120000,
  188. isMetricsData: false,
  189. },
  190. });
  191. });
  192. it('renders Edit and Open buttons', async function () {
  193. mockEvents();
  194. await renderModal({initialData, widget: mockWidget});
  195. expect(screen.getByText('Edit Widget')).toBeInTheDocument();
  196. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  197. });
  198. it('renders updated table columns and orderby', async function () {
  199. const eventsMock = mockEvents();
  200. await renderModal({initialData, widget: mockWidget});
  201. expect(screen.getByText('title')).toBeInTheDocument();
  202. expect(screen.getByText('/organizations/:orgId/dashboards/')).toBeInTheDocument();
  203. expect(eventsMock).toHaveBeenCalledWith(
  204. '/organizations/org-slug/events/',
  205. expect.objectContaining({
  206. query: expect.objectContaining({sort: ['-count()']}),
  207. })
  208. );
  209. });
  210. it('applies the dashboard filters to the widget query when provided', async function () {
  211. const eventsMock = mockEvents();
  212. await renderModal({
  213. initialData,
  214. widget: mockWidget,
  215. dashboardFilters: {release: ['project-release@1.2.0']},
  216. });
  217. expect(screen.getByText('title')).toBeInTheDocument();
  218. expect(screen.getByText('/organizations/:orgId/dashboards/')).toBeInTheDocument();
  219. expect(eventsMock).toHaveBeenCalledWith(
  220. '/organizations/org-slug/events/',
  221. expect.objectContaining({
  222. query: expect.objectContaining({
  223. query:
  224. // The release was injected into the discover query
  225. 'title:/organizations/:orgId/performance/summary/ release:"project-release@1.2.0" ',
  226. }),
  227. })
  228. );
  229. });
  230. it('renders area chart', async function () {
  231. mockEvents();
  232. await renderModal({initialData, widget: mockWidget});
  233. expect(screen.getByText('echarts mock')).toBeInTheDocument();
  234. });
  235. it('renders description', async function () {
  236. mockEvents();
  237. await renderModal({
  238. initialData,
  239. widget: {...mockWidget, description: 'This is a description'},
  240. });
  241. expect(screen.getByText('This is a description')).toBeInTheDocument();
  242. });
  243. it('renders Discover area chart widget viewer', async function () {
  244. mockEvents();
  245. await renderModal({initialData, widget: mockWidget});
  246. });
  247. it('redirects user to Discover when clicking Open in Discover', async function () {
  248. mockEvents();
  249. await renderModal({initialData, widget: mockWidget});
  250. await userEvent.click(screen.getByText('Open in Discover'));
  251. expect(initialData.router.push).toHaveBeenCalledWith(
  252. '/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'
  253. );
  254. });
  255. it('zooms into the selected time range', async function () {
  256. mockEvents();
  257. await renderModal({initialData, widget: mockWidget});
  258. act(() => {
  259. // Simulate dataZoom event on chart
  260. (ReactEchartsCore as jest.Mock).mock.calls[0][0].onEvents.datazoom(undefined, {
  261. getModel: () => {
  262. return {
  263. _payload: {
  264. batch: [{startValue: 1646100000000, endValue: 1646120000000}],
  265. },
  266. };
  267. },
  268. });
  269. });
  270. expect(initialData.router.push).toHaveBeenCalledWith(
  271. expect.objectContaining({
  272. query: {
  273. viewerEnd: '2022-03-01T07:33:20',
  274. viewerStart: '2022-03-01T02:00:00',
  275. },
  276. })
  277. );
  278. });
  279. it('renders multiquery label and selector', async function () {
  280. mockEvents();
  281. await renderModal({initialData, widget: mockWidget});
  282. expect(
  283. screen.getByText(
  284. 'This widget was built with multiple queries. Table data can only be displayed for one query at a time. To edit any of the queries, edit the widget.'
  285. )
  286. ).toBeInTheDocument();
  287. expect(screen.getByText('Query Name')).toBeInTheDocument();
  288. });
  289. it('updates selected query when selected in the query dropdown', async function () {
  290. mockEvents();
  291. const {rerender} = await renderModal({initialData, widget: mockWidget});
  292. await userEvent.click(screen.getByText('Query Name'));
  293. await userEvent.click(screen.getByText('Another Query Name'));
  294. expect(initialData.router.replace).toHaveBeenCalledWith({
  295. query: {query: 1},
  296. });
  297. // Need to manually set the new router location and rerender to simulate the dropdown selection click
  298. initialData.router.location.query = {query: ['1']};
  299. rerender(
  300. <WidgetViewerModal
  301. Header={stubEl}
  302. Footer={stubEl as ModalRenderProps['Footer']}
  303. Body={stubEl as ModalRenderProps['Body']}
  304. CloseButton={stubEl}
  305. closeModal={() => undefined}
  306. organization={initialData.organization}
  307. widget={mockWidget}
  308. onEdit={() => undefined}
  309. />
  310. );
  311. await waitForMetaToHaveBeenCalled();
  312. expect(screen.getByText('Another Query Name')).toBeInTheDocument();
  313. });
  314. it('renders the correct discover query link when there are multiple queries in a widget', async function () {
  315. mockEvents();
  316. initialData.router.location.query = {query: ['1']};
  317. await renderModal({initialData, widget: mockWidget});
  318. expect(screen.getByRole('button', {name: 'Open in Discover'})).toHaveAttribute(
  319. 'href',
  320. '/organizations/org-slug/discover/results/?environment=prod&environment=dev&field=count%28%29&name=Test%20Widget&project=1&project=2&query=&statsPeriod=24h&yAxis=count%28%29'
  321. );
  322. });
  323. it('renders with first legend disabled by default', async function () {
  324. mockEvents();
  325. // Rerender with first legend disabled
  326. initialData.router.location.query = {legend: ['Query Name']};
  327. await renderModal({initialData, widget: mockWidget});
  328. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  329. expect.objectContaining({
  330. option: expect.objectContaining({
  331. legend: expect.objectContaining({
  332. selected: {'Query Name': false},
  333. }),
  334. }),
  335. }),
  336. {}
  337. );
  338. });
  339. it('renders total results in footer', async function () {
  340. mockEvents();
  341. await renderModal({initialData, widget: mockWidget});
  342. expect(screen.getByText('33,323,612')).toBeInTheDocument();
  343. });
  344. it('renders highlighted query text and multiple queries in select dropdown', async function () {
  345. mockEvents();
  346. await renderModal({
  347. initialData,
  348. widget: {
  349. ...mockWidget,
  350. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  351. },
  352. });
  353. await userEvent.click(
  354. screen.getByText('/organizations/:orgId/performance/summary/')
  355. );
  356. });
  357. it('renders widget chart minimap', async function () {
  358. initialData.organization.features.push('widget-viewer-modal-minimap');
  359. mockEvents();
  360. await renderModal({
  361. initialData,
  362. widget: {
  363. ...mockWidget,
  364. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  365. },
  366. });
  367. expect(ReactEchartsCore).toHaveBeenLastCalledWith(
  368. expect.objectContaining({
  369. option: expect.objectContaining({
  370. dataZoom: expect.arrayContaining([
  371. expect.objectContaining({
  372. realtime: false,
  373. showDetail: false,
  374. end: 100,
  375. start: 0,
  376. }),
  377. ]),
  378. }),
  379. }),
  380. {}
  381. );
  382. });
  383. it('zooming on minimap updates location query and updates echart start and end values', async function () {
  384. initialData.organization.features.push('widget-viewer-modal-minimap');
  385. mockEvents();
  386. await renderModal({
  387. initialData,
  388. widget: {
  389. ...mockWidget,
  390. queries: [{...mockQuery, name: ''}, additionalMockQuery],
  391. },
  392. });
  393. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  394. act(() => {
  395. // Simulate dataZoom event on chart
  396. calls[calls.length - 1][0].onEvents.datazoom(
  397. {seriesStart: 1646100000000, seriesEnd: 1646120000000},
  398. {
  399. getModel: () => {
  400. return {
  401. _payload: {start: 30, end: 70},
  402. };
  403. },
  404. }
  405. );
  406. });
  407. expect(initialData.router.push).toHaveBeenCalledWith(
  408. expect.objectContaining({
  409. query: {
  410. viewerEnd: '2022-03-01T05:53:20',
  411. viewerStart: '2022-03-01T03:40:00',
  412. },
  413. })
  414. );
  415. });
  416. it('includes group by in widget viewer table', async function () {
  417. mockEvents();
  418. mockWidget.queries = [
  419. {
  420. conditions: 'title:/organizations/:orgId/performance/summary/',
  421. fields: ['count()'],
  422. aggregates: ['count()'],
  423. columns: ['transaction'],
  424. name: 'Query Name',
  425. orderby: '-count()',
  426. },
  427. ];
  428. await renderModal({initialData, widget: mockWidget});
  429. screen.getByText('transaction');
  430. });
  431. it('includes order by in widget viewer table if not explicitly selected', async function () {
  432. mockEvents();
  433. mockWidget.queries = [
  434. {
  435. conditions: 'title:/organizations/:orgId/performance/summary/',
  436. fields: ['count()'],
  437. aggregates: ['count()'],
  438. columns: ['transaction'],
  439. name: 'Query Name',
  440. orderby: 'count_unique(user)',
  441. },
  442. ];
  443. await renderModal({initialData, widget: mockWidget});
  444. screen.getByText('count_unique(user)');
  445. });
  446. it('includes a custom equation order by in widget viewer table if not explicitly selected', async function () {
  447. mockEvents();
  448. mockWidget.queries = [
  449. {
  450. conditions: 'title:/organizations/:orgId/performance/summary/',
  451. fields: ['count()'],
  452. aggregates: ['count()'],
  453. columns: ['transaction'],
  454. name: 'Query Name',
  455. orderby: '-equation|count_unique(user) + 1',
  456. },
  457. ];
  458. await renderModal({initialData, widget: mockWidget});
  459. screen.getByText('count_unique(user) + 1');
  460. });
  461. it('renders widget chart with y axis formatter using provided seriesResultType', async function () {
  462. mockEvents();
  463. await renderModal({
  464. initialData: initialDataWithFlag,
  465. widget: mockWidget,
  466. seriesData: [],
  467. seriesResultsType: {'count()': 'duration', 'count_unique()': 'duration'},
  468. });
  469. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  470. const yAxisFormatter =
  471. calls[calls.length - 1][0].option.yAxis.axisLabel.formatter;
  472. expect(yAxisFormatter(123)).toEqual('123ms');
  473. });
  474. it('renders widget chart with default number y axis formatter when seriesResultType has multiple different types', async function () {
  475. mockEvents();
  476. await renderModal({
  477. initialData: initialDataWithFlag,
  478. widget: mockWidget,
  479. seriesData: [],
  480. seriesResultsType: {'count()': 'duration', 'count_unique()': 'size'},
  481. });
  482. const calls = (ReactEchartsCore as jest.Mock).mock.calls;
  483. const yAxisFormatter =
  484. calls[calls.length - 1][0].option.yAxis.axisLabel.formatter;
  485. expect(yAxisFormatter(123)).toEqual('123');
  486. });
  487. it('does not allow sorting by transaction name when widget is using metrics', async function () {
  488. const eventsMock = MockApiClient.addMockResponse({
  489. url: '/organizations/org-slug/events/',
  490. body: {
  491. data: [
  492. {
  493. title: '/organizations/:orgId/dashboards/',
  494. id: '1',
  495. count: 1,
  496. },
  497. ],
  498. meta: {
  499. fields: {
  500. title: 'string',
  501. id: 'string',
  502. count: 1,
  503. },
  504. isMetricsData: true,
  505. },
  506. },
  507. });
  508. await renderModal({
  509. initialData: initialDataWithFlag,
  510. widget: mockWidget,
  511. seriesData: [],
  512. seriesResultsType: {'count()': 'duration'},
  513. });
  514. expect(eventsMock).toHaveBeenCalledTimes(1);
  515. expect(screen.getByText('title')).toBeInTheDocument();
  516. await userEvent.click(screen.getByText('title'));
  517. expect(initialData.router.push).not.toHaveBeenCalledWith({
  518. query: {sort: ['-title']},
  519. });
  520. });
  521. it('renders transaction summary link', async function () {
  522. ProjectsStore.loadInitialData(initialData.organization.projects);
  523. MockApiClient.addMockResponse({
  524. url: '/organizations/org-slug/events/',
  525. body: {
  526. data: [
  527. {
  528. title: '/organizations/:orgId/dashboards/',
  529. transaction: '/discover/homepage/',
  530. project: 'project-slug',
  531. id: '1',
  532. },
  533. ],
  534. meta: {
  535. fields: {
  536. title: 'string',
  537. transaction: 'string',
  538. project: 'string',
  539. id: 'string',
  540. },
  541. isMetricsData: true,
  542. },
  543. },
  544. });
  545. mockWidget.queries = [
  546. {
  547. conditions: 'title:/organizations/:orgId/performance/summary/',
  548. fields: [''],
  549. aggregates: [''],
  550. columns: ['transaction'],
  551. name: 'Query Name',
  552. orderby: '',
  553. },
  554. ];
  555. await renderModal({
  556. initialData: initialDataWithFlag,
  557. widget: mockWidget,
  558. seriesData: [],
  559. seriesResultsType: {'count()': 'duration'},
  560. });
  561. const link = screen.getByTestId('widget-viewer-transaction-link');
  562. expect(link).toHaveAttribute(
  563. 'href',
  564. expect.stringMatching(
  565. RegExp(
  566. '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
  567. )
  568. )
  569. );
  570. });
  571. });
  572. describe('TopN Chart Widget', function () {
  573. let mockQuery, mockWidget;
  574. function mockEventsStats() {
  575. return MockApiClient.addMockResponse({
  576. url: '/organizations/org-slug/events-stats/',
  577. body: {
  578. data: [
  579. [[1646100000], [{count: 1}]],
  580. [[1646120000], [{count: 1}]],
  581. ],
  582. start: 1646100000,
  583. end: 1646120000,
  584. isMetricsData: false,
  585. },
  586. });
  587. }
  588. const eventsMockData = [
  589. {
  590. 'error.type': ['Test Error 1a', 'Test Error 1b', 'Test Error 1c'],
  591. count: 10,
  592. },
  593. {
  594. 'error.type': ['Test Error 2'],
  595. count: 6,
  596. },
  597. {
  598. 'error.type': ['Test Error 3'],
  599. count: 5,
  600. },
  601. {
  602. 'error.type': ['Test Error 4'],
  603. count: 4,
  604. },
  605. {
  606. 'error.type': ['Test Error 5'],
  607. count: 3,
  608. },
  609. {
  610. 'error.type': ['Test Error 6'],
  611. count: 2,
  612. },
  613. ];
  614. function mockEvents() {
  615. return MockApiClient.addMockResponse({
  616. url: '/organizations/org-slug/events/',
  617. match: [MockApiClient.matchQuery({cursor: undefined})],
  618. headers: {
  619. Link:
  620. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  621. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  622. },
  623. body: {
  624. data: eventsMockData,
  625. meta: {
  626. fields: {
  627. 'error.type': 'array',
  628. count: 'integer',
  629. },
  630. },
  631. },
  632. });
  633. }
  634. beforeEach(function () {
  635. mockQuery = {
  636. conditions: 'title:/organizations/:orgId/performance/summary/',
  637. fields: ['error.type', 'count()'],
  638. aggregates: ['count()'],
  639. columns: ['error.type'],
  640. id: '1',
  641. name: 'Query Name',
  642. orderby: '',
  643. };
  644. mockWidget = {
  645. title: 'Test Widget',
  646. displayType: DisplayType.TOP_N,
  647. interval: '5m',
  648. queries: [mockQuery],
  649. widgetType: WidgetType.DISCOVER,
  650. };
  651. MockApiClient.addMockResponse({
  652. url: '/organizations/org-slug/events/',
  653. match: [MockApiClient.matchQuery({cursor: '0:10:0'})],
  654. headers: {
  655. Link:
  656. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  657. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:20:0>; rel="next"; results="true"; cursor="0:20:0"',
  658. },
  659. body: {
  660. data: [
  661. {
  662. 'error.type': ['Next Page Test Error'],
  663. count: 1,
  664. },
  665. ],
  666. meta: {
  667. fields: {
  668. 'error.type': 'array',
  669. count: 'integer',
  670. },
  671. },
  672. },
  673. });
  674. });
  675. it('renders Discover topn chart widget viewer', async function () {
  676. mockEventsStats();
  677. mockEvents();
  678. 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(screen.getByText('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(screen.getByLabelText('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(screen.getByText('Issue Widget')).toBeInTheDocument();
  1063. });
  1064. it('renders Edit and Open buttons', async function () {
  1065. await renderModal({initialData, widget: mockWidget});
  1066. expect(screen.getByText('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(screen.getByText('title')).toBeInTheDocument();
  1072. expect(await screen.findByText('Error: Failed')).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. expect(issuesMock).toHaveBeenCalledWith(
  1110. '/organizations/org-slug/issues/',
  1111. expect.objectContaining({
  1112. data: {
  1113. cursor: undefined,
  1114. environment: ['prod', 'dev'],
  1115. expand: ['owners'],
  1116. limit: 20,
  1117. project: [1, 2],
  1118. query: 'is:unresolved',
  1119. sort: 'date',
  1120. statsPeriod: '24h',
  1121. },
  1122. })
  1123. );
  1124. });
  1125. it('renders pagination buttons', async function () {
  1126. await renderModal({initialData, widget: mockWidget});
  1127. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1128. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1129. });
  1130. it('paginates to the next page', async function () {
  1131. const {rerender} = await renderModal({initialData, widget: mockWidget});
  1132. expect(await screen.findByText('Error: Failed')).toBeInTheDocument();
  1133. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  1134. expect(issuesMock).toHaveBeenCalledTimes(1);
  1135. expect(initialData.router.replace).toHaveBeenCalledWith(
  1136. expect.objectContaining({
  1137. query: {cursor: '0:10:0', page: 1},
  1138. })
  1139. );
  1140. // Need to manually set the new router location and rerender to simulate the next page click
  1141. initialData.router.location.query = {cursor: ['0:10:0']};
  1142. rerender(
  1143. <WidgetViewerModal
  1144. Header={stubEl}
  1145. Footer={stubEl as ModalRenderProps['Footer']}
  1146. Body={stubEl as ModalRenderProps['Body']}
  1147. CloseButton={stubEl}
  1148. closeModal={() => undefined}
  1149. organization={initialData.organization}
  1150. widget={mockWidget}
  1151. onEdit={() => undefined}
  1152. />
  1153. );
  1154. expect(await screen.findByText('Another Error: Failed')).toBeInTheDocument();
  1155. });
  1156. it('displays with correct table column widths', async function () {
  1157. initialData.router.location.query = {width: ['-1', '-1', '575']};
  1158. await renderModal({initialData, widget: mockWidget});
  1159. expect(screen.getByTestId('grid-editable')).toHaveStyle({
  1160. 'grid-template-columns':
  1161. ' minmax(90px, auto) minmax(90px, auto) minmax(575px, auto)',
  1162. });
  1163. });
  1164. it('uses provided tableData and does not make an issues requests', async function () {
  1165. await renderModal({initialData, widget: mockWidget, tableData: []});
  1166. expect(issuesMock).not.toHaveBeenCalled();
  1167. });
  1168. it('makes issues requests when table is sorted', async function () {
  1169. await renderModal({
  1170. initialData,
  1171. widget: mockWidget,
  1172. tableData: [],
  1173. });
  1174. expect(issuesMock).not.toHaveBeenCalled();
  1175. await userEvent.click(screen.getByText('events'));
  1176. await waitFor(() => {
  1177. expect(issuesMock).toHaveBeenCalled();
  1178. });
  1179. });
  1180. });
  1181. describe('Release Health Widgets', function () {
  1182. let metricsMock;
  1183. const mockQuery = {
  1184. conditions: '',
  1185. fields: [`sum(session)`],
  1186. columns: [],
  1187. aggregates: [],
  1188. id: '1',
  1189. name: 'Query Name',
  1190. orderby: '',
  1191. };
  1192. const mockWidget = {
  1193. id: '1',
  1194. title: 'Release Widget',
  1195. displayType: DisplayType.LINE,
  1196. interval: '5m',
  1197. queries: [mockQuery],
  1198. widgetType: WidgetType.RELEASE,
  1199. };
  1200. beforeEach(function () {
  1201. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  1202. metricsMock = MockApiClient.addMockResponse({
  1203. url: '/organizations/org-slug/metrics/data/',
  1204. body: MetricsTotalCountByReleaseIn24h(),
  1205. headers: {
  1206. link:
  1207. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1208. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  1209. },
  1210. });
  1211. });
  1212. afterEach(() => {
  1213. jest.useRealTimers();
  1214. });
  1215. it('does a sessions query', async function () {
  1216. await renderModal({initialData, widget: mockWidget});
  1217. expect(metricsMock).toHaveBeenCalled();
  1218. });
  1219. it('renders widget title', async function () {
  1220. await renderModal({initialData, widget: mockWidget});
  1221. expect(screen.getByText('Release Widget')).toBeInTheDocument();
  1222. });
  1223. it('renders Edit and Open in Releases buttons', async function () {
  1224. await renderModal({initialData, widget: mockWidget});
  1225. expect(screen.getByText('Edit Widget')).toBeInTheDocument();
  1226. expect(screen.getByText('Open in Releases')).toBeInTheDocument();
  1227. });
  1228. it('Open in Releases button redirects browser', async function () {
  1229. await renderModal({initialData, widget: mockWidget});
  1230. await userEvent.click(screen.getByText('Open in Releases'), {delay: null});
  1231. expect(initialData.router.push).toHaveBeenCalledWith(
  1232. '/organizations/org-slug/releases/?environment=prod&environment=dev&project=1&project=2&statsPeriod=24h'
  1233. );
  1234. });
  1235. it('renders table header and body', async function () {
  1236. await renderModal({initialData, widget: mockWidget});
  1237. expect(screen.getByText('release')).toBeInTheDocument();
  1238. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1239. expect(screen.getByText('sum(session)')).toBeInTheDocument();
  1240. expect(screen.getByText('6.3k')).toBeInTheDocument();
  1241. });
  1242. it('renders Release widget viewer', async function () {
  1243. await renderModal({initialData, widget: mockWidget});
  1244. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1245. });
  1246. it('renders pagination buttons', async function () {
  1247. await renderModal({
  1248. initialData,
  1249. widget: mockWidget,
  1250. });
  1251. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1252. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1253. });
  1254. it('does not render pagination buttons when sorting by release', async function () {
  1255. await renderModal({
  1256. initialData,
  1257. widget: {...mockWidget, queries: [{...mockQuery, orderby: 'release'}]},
  1258. });
  1259. expect(screen.queryByRole('button', {name: 'Previous'})).not.toBeInTheDocument();
  1260. expect(screen.queryByRole('button', {name: 'Next'})).not.toBeInTheDocument();
  1261. });
  1262. it('makes a new sessions request after sorting by a table column', async function () {
  1263. const {rerender} = await renderModal({
  1264. initialData,
  1265. widget: mockWidget,
  1266. tableData: [],
  1267. seriesData: [],
  1268. });
  1269. expect(metricsMock).toHaveBeenCalledTimes(1);
  1270. await userEvent.click(screen.getByText(`sum(session)`), {delay: null});
  1271. expect(initialData.router.push).toHaveBeenCalledWith({
  1272. query: {sort: '-sum(session)'},
  1273. });
  1274. // Need to manually set the new router location and rerender to simulate the sortable column click
  1275. initialData.router.location.query = {sort: '-sum(session)'};
  1276. rerender(
  1277. <WidgetViewerModal
  1278. Header={stubEl}
  1279. Footer={stubEl as ModalRenderProps['Footer']}
  1280. Body={stubEl as ModalRenderProps['Body']}
  1281. CloseButton={stubEl}
  1282. closeModal={() => undefined}
  1283. organization={initialData.organization}
  1284. widget={mockWidget}
  1285. onEdit={() => undefined}
  1286. seriesData={[]}
  1287. tableData={[]}
  1288. />
  1289. );
  1290. await waitFor(() => {
  1291. expect(metricsMock).toHaveBeenCalledTimes(2);
  1292. });
  1293. });
  1294. });
  1295. });