widgetViewerModal.spec.tsx 54 KB

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