widgetViewerModal.spec.tsx 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389
  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. await act(tick);
  913. expect(eventsMock).not.toHaveBeenCalled();
  914. await userEvent.click(await screen.findByLabelText('Next'));
  915. await waitFor(() => {
  916. expect(eventsMock).toHaveBeenCalled();
  917. });
  918. });
  919. it('displays table data with units correctly', async function () {
  920. const eventsMock = MockApiClient.addMockResponse({
  921. url: '/organizations/org-slug/events/',
  922. match: [MockApiClient.matchQuery({cursor: undefined})],
  923. headers: {
  924. Link:
  925. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  926. '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  927. },
  928. body: {
  929. data: [
  930. {
  931. 'p75(measurements.custom.minute)': 94.87035966318831,
  932. 'p95(measurements.custom.ratio)': 0.9881980140455187,
  933. 'p75(measurements.custom.kibibyte)': 217.87035966318834,
  934. },
  935. ],
  936. meta: {
  937. fields: {
  938. 'p75(measurements.custom.minute)': 'duration',
  939. 'p95(measurements.custom.ratio)': 'percentage',
  940. 'p75(measurements.custom.kibibyte)': 'size',
  941. },
  942. units: {
  943. 'p75(measurements.custom.minute)': 'minute',
  944. 'p95(measurements.custom.ratio)': null,
  945. 'p75(measurements.custom.kibibyte)': 'kibibyte',
  946. },
  947. isMetricsData: true,
  948. tips: {},
  949. },
  950. },
  951. });
  952. await renderModal({
  953. initialData: initialDataWithFlag,
  954. widget: {
  955. title: 'Custom Widget',
  956. displayType: 'table',
  957. queries: [
  958. {
  959. fields: [
  960. 'p75(measurements.custom.kibibyte)',
  961. 'p75(measurements.custom.minute)',
  962. 'p95(measurements.custom.ratio)',
  963. ],
  964. aggregates: [
  965. 'p75(measurements.custom.kibibyte)',
  966. 'p75(measurements.custom.minute)',
  967. 'p95(measurements.custom.ratio)',
  968. ],
  969. columns: [],
  970. orderby: '-p75(measurements.custom.kibibyte)',
  971. },
  972. ],
  973. widgetType: 'discover',
  974. },
  975. });
  976. await waitFor(() => {
  977. expect(eventsMock).toHaveBeenCalled();
  978. });
  979. expect(screen.getByText('217.9 KiB')).toBeInTheDocument();
  980. expect(screen.getByText('1.58hr')).toBeInTheDocument();
  981. expect(screen.getByText('98.82%')).toBeInTheDocument();
  982. });
  983. });
  984. });
  985. describe('Issue Table Widget', function () {
  986. let issuesMock;
  987. const mockQuery = {
  988. conditions: 'is:unresolved',
  989. fields: ['events', 'status', 'title'],
  990. columns: ['events', 'status', 'title'],
  991. aggregates: [],
  992. id: '1',
  993. name: 'Query Name',
  994. orderby: '',
  995. };
  996. const mockWidget = {
  997. id: '1',
  998. title: 'Issue Widget',
  999. displayType: DisplayType.TABLE,
  1000. interval: '5m',
  1001. queries: [mockQuery],
  1002. widgetType: WidgetType.ISSUE,
  1003. };
  1004. beforeEach(function () {
  1005. MemberListStore.loadInitialData([]);
  1006. MockApiClient.addMockResponse({
  1007. url: '/organizations/org-slug/issues/',
  1008. method: 'GET',
  1009. match: [
  1010. MockApiClient.matchData({
  1011. cursor: '0:10:0',
  1012. }),
  1013. ],
  1014. headers: {
  1015. Link:
  1016. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1017. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:20:0>; rel="next"; results="true"; cursor="0:20:0"',
  1018. },
  1019. body: [
  1020. {
  1021. id: '2',
  1022. title: 'Another Error: Failed',
  1023. project: {
  1024. id: '3',
  1025. },
  1026. status: 'unresolved',
  1027. lifetime: {count: 5},
  1028. count: 3,
  1029. userCount: 1,
  1030. },
  1031. ],
  1032. });
  1033. issuesMock = MockApiClient.addMockResponse({
  1034. url: '/organizations/org-slug/issues/',
  1035. method: 'GET',
  1036. match: [
  1037. MockApiClient.matchData({
  1038. cursor: undefined,
  1039. }),
  1040. ],
  1041. headers: {
  1042. Link:
  1043. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1044. '<http://localhost/api/0/organizations/org-slug/issues/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  1045. },
  1046. body: [
  1047. {
  1048. id: '1',
  1049. title: 'Error: Failed',
  1050. project: {
  1051. id: '3',
  1052. },
  1053. status: 'unresolved',
  1054. lifetime: {count: 10},
  1055. count: 6,
  1056. userCount: 3,
  1057. },
  1058. ],
  1059. });
  1060. });
  1061. it('renders widget title', async function () {
  1062. await renderModal({initialData, widget: mockWidget});
  1063. expect(await screen.findByText('Issue Widget')).toBeInTheDocument();
  1064. });
  1065. it('renders Edit and Open buttons', async function () {
  1066. await renderModal({initialData, widget: mockWidget});
  1067. expect(await screen.findByText('Edit Widget')).toBeInTheDocument();
  1068. expect(screen.getByText('Open in Issues')).toBeInTheDocument();
  1069. });
  1070. it('renders events, status, async and title table columns', async function () {
  1071. await renderModal({initialData, widget: mockWidget});
  1072. expect(await screen.findByText('Error: Failed')).toBeInTheDocument();
  1073. expect(screen.getByText('title')).toBeInTheDocument();
  1074. expect(screen.getByText('events')).toBeInTheDocument();
  1075. expect(screen.getByText('6')).toBeInTheDocument();
  1076. expect(screen.getByText('status')).toBeInTheDocument();
  1077. expect(screen.getByText('unresolved')).toBeInTheDocument();
  1078. });
  1079. it('renders Issue table widget viewer', async function () {
  1080. await renderModal({initialData, widget: mockWidget});
  1081. await screen.findByText('Error: Failed');
  1082. });
  1083. it('redirects user to Issues when clicking Open in Issues', async function () {
  1084. await renderModal({initialData, widget: mockWidget});
  1085. await userEvent.click(screen.getByText('Open in Issues'));
  1086. expect(initialData.router.push).toHaveBeenCalledWith(
  1087. '/organizations/org-slug/issues/?environment=prod&environment=dev&project=1&project=2&query=is%3Aunresolved&sort=&statsPeriod=24h'
  1088. );
  1089. });
  1090. it('sorts table when a sortable column header is clicked', async function () {
  1091. const {rerender} = await renderModal({initialData, widget: mockWidget});
  1092. await userEvent.click(screen.getByText('events'));
  1093. expect(initialData.router.push).toHaveBeenCalledWith({
  1094. query: {sort: 'freq'},
  1095. });
  1096. // Need to manually set the new router location and rerender to simulate the sortable column click
  1097. initialData.router.location.query = {sort: ['freq']};
  1098. rerender(
  1099. <WidgetViewerModal
  1100. Header={stubEl}
  1101. Footer={stubEl as ModalRenderProps['Footer']}
  1102. Body={stubEl as ModalRenderProps['Body']}
  1103. CloseButton={stubEl}
  1104. closeModal={() => undefined}
  1105. organization={initialData.organization}
  1106. widget={mockWidget}
  1107. onEdit={() => undefined}
  1108. />
  1109. );
  1110. await waitFor(() =>
  1111. expect(issuesMock).toHaveBeenCalledWith(
  1112. '/organizations/org-slug/issues/',
  1113. expect.objectContaining({
  1114. data: {
  1115. cursor: undefined,
  1116. environment: ['prod', 'dev'],
  1117. expand: ['owners'],
  1118. limit: 20,
  1119. project: [1, 2],
  1120. query: 'is:unresolved',
  1121. sort: 'date',
  1122. statsPeriod: '24h',
  1123. },
  1124. })
  1125. )
  1126. );
  1127. });
  1128. it('renders pagination buttons', async function () {
  1129. await renderModal({initialData, widget: mockWidget});
  1130. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1131. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1132. });
  1133. it('paginates to the next page', async function () {
  1134. const {rerender} = await renderModal({initialData, widget: mockWidget});
  1135. expect(await screen.findByText('Error: Failed')).toBeInTheDocument();
  1136. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  1137. expect(issuesMock).toHaveBeenCalledTimes(1);
  1138. expect(initialData.router.replace).toHaveBeenCalledWith(
  1139. expect.objectContaining({
  1140. query: {cursor: '0:10:0', page: 1},
  1141. })
  1142. );
  1143. // Need to manually set the new router location and rerender to simulate the next page click
  1144. initialData.router.location.query = {cursor: ['0:10:0']};
  1145. rerender(
  1146. <WidgetViewerModal
  1147. Header={stubEl}
  1148. Footer={stubEl as ModalRenderProps['Footer']}
  1149. Body={stubEl as ModalRenderProps['Body']}
  1150. CloseButton={stubEl}
  1151. closeModal={() => undefined}
  1152. organization={initialData.organization}
  1153. widget={mockWidget}
  1154. onEdit={() => undefined}
  1155. />
  1156. );
  1157. expect(await screen.findByText('Another Error: Failed')).toBeInTheDocument();
  1158. });
  1159. it('displays with correct table column widths', async function () {
  1160. initialData.router.location.query = {width: ['-1', '-1', '575']};
  1161. await renderModal({initialData, widget: mockWidget});
  1162. expect(await screen.findByTestId('grid-editable')).toHaveStyle({
  1163. 'grid-template-columns':
  1164. ' minmax(90px, auto) minmax(90px, auto) minmax(575px, auto)',
  1165. });
  1166. });
  1167. it('uses provided tableData and does not make an issues requests', async function () {
  1168. await renderModal({initialData, widget: mockWidget, tableData: []});
  1169. expect(issuesMock).not.toHaveBeenCalled();
  1170. });
  1171. it('makes issues requests when table is sorted', async function () {
  1172. await renderModal({
  1173. initialData,
  1174. widget: mockWidget,
  1175. tableData: [],
  1176. });
  1177. expect(issuesMock).not.toHaveBeenCalled();
  1178. await userEvent.click(screen.getByText('events'));
  1179. await waitFor(() => {
  1180. expect(issuesMock).toHaveBeenCalled();
  1181. });
  1182. });
  1183. });
  1184. describe('Release Health Widgets', function () {
  1185. let metricsMock;
  1186. const mockQuery = {
  1187. conditions: '',
  1188. fields: [`sum(session)`],
  1189. columns: [],
  1190. aggregates: [],
  1191. id: '1',
  1192. name: 'Query Name',
  1193. orderby: '',
  1194. };
  1195. const mockWidget = {
  1196. id: '1',
  1197. title: 'Release Widget',
  1198. displayType: DisplayType.LINE,
  1199. interval: '5m',
  1200. queries: [mockQuery],
  1201. widgetType: WidgetType.RELEASE,
  1202. };
  1203. beforeEach(function () {
  1204. setMockDate(new Date('2022-08-02'));
  1205. metricsMock = MockApiClient.addMockResponse({
  1206. url: '/organizations/org-slug/metrics/data/',
  1207. body: MetricsTotalCountByReleaseIn24h(),
  1208. headers: {
  1209. link:
  1210. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1",' +
  1211. '<http://localhost/api/0/organizations/org-slug/metrics/data/?cursor=0:10:0>; rel="next"; results="true"; cursor="0:10:0"',
  1212. },
  1213. });
  1214. });
  1215. afterEach(() => {
  1216. resetMockDate();
  1217. });
  1218. it('does a sessions query', async function () {
  1219. await renderModal({initialData, widget: mockWidget});
  1220. await waitFor(() => {
  1221. expect(metricsMock).toHaveBeenCalled();
  1222. });
  1223. });
  1224. it('renders widget title', async function () {
  1225. await renderModal({initialData, widget: mockWidget});
  1226. expect(await screen.findByText('Release Widget')).toBeInTheDocument();
  1227. });
  1228. it('renders Edit and Open in Releases buttons', async function () {
  1229. await renderModal({initialData, widget: mockWidget});
  1230. expect(await screen.findByText('Edit Widget')).toBeInTheDocument();
  1231. expect(screen.getByText('Open in Releases')).toBeInTheDocument();
  1232. });
  1233. it('Open in Releases button redirects browser', async function () {
  1234. await renderModal({initialData, widget: mockWidget});
  1235. await userEvent.click(screen.getByText('Open in Releases'), {delay: null});
  1236. expect(initialData.router.push).toHaveBeenCalledWith(
  1237. '/organizations/org-slug/releases/?environment=prod&environment=dev&project=1&project=2&statsPeriod=24h'
  1238. );
  1239. });
  1240. it('renders table header and body', async function () {
  1241. await renderModal({initialData, widget: mockWidget});
  1242. expect(screen.getByText('release')).toBeInTheDocument();
  1243. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1244. expect(screen.getByText('sum(session)')).toBeInTheDocument();
  1245. expect(screen.getByText('6.3k')).toBeInTheDocument();
  1246. });
  1247. it('renders Release widget viewer', async function () {
  1248. await renderModal({initialData, widget: mockWidget});
  1249. expect(await screen.findByText('e102abb2c46e')).toBeInTheDocument();
  1250. });
  1251. it('renders pagination buttons', async function () {
  1252. await renderModal({
  1253. initialData,
  1254. widget: mockWidget,
  1255. });
  1256. expect(await screen.findByRole('button', {name: 'Previous'})).toBeInTheDocument();
  1257. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  1258. });
  1259. it('does not render pagination buttons when sorting by release', async function () {
  1260. // TODO(scttcper): We shouldn't need to wrap render with act, it seems to double render ReleaseWidgetQueries
  1261. await act(() =>
  1262. renderModal({
  1263. initialData,
  1264. widget: {...mockWidget, queries: [{...mockQuery, orderby: 'release'}]},
  1265. // in react 17 act requires that nothing is returned
  1266. }).then(() => void 0)
  1267. );
  1268. expect(screen.queryByRole('button', {name: 'Previous'})).not.toBeInTheDocument();
  1269. expect(screen.queryByRole('button', {name: 'Next'})).not.toBeInTheDocument();
  1270. });
  1271. it('makes a new sessions request after sorting by a table column', async function () {
  1272. const {rerender} = await renderModal({
  1273. initialData,
  1274. widget: mockWidget,
  1275. tableData: [],
  1276. seriesData: [],
  1277. });
  1278. expect(metricsMock).toHaveBeenCalledTimes(1);
  1279. await userEvent.click(await screen.findByText(`sum(session)`), {delay: null});
  1280. expect(initialData.router.push).toHaveBeenCalledWith({
  1281. query: {sort: '-sum(session)'},
  1282. });
  1283. // Need to manually set the new router location and rerender to simulate the sortable column click
  1284. initialData.router.location.query = {sort: '-sum(session)'};
  1285. rerender(
  1286. <WidgetViewerModal
  1287. Header={stubEl}
  1288. Footer={stubEl as ModalRenderProps['Footer']}
  1289. Body={stubEl as ModalRenderProps['Body']}
  1290. CloseButton={stubEl}
  1291. closeModal={() => undefined}
  1292. organization={initialData.organization}
  1293. widget={mockWidget}
  1294. onEdit={() => undefined}
  1295. seriesData={[]}
  1296. tableData={[]}
  1297. />
  1298. );
  1299. await waitFor(() => {
  1300. expect(metricsMock).toHaveBeenCalledTimes(2);
  1301. });
  1302. });
  1303. });
  1304. });