transactionsList.spec.jsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. import {act} from 'react-dom/test-utils';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {triggerPress} from 'sentry-test/utils';
  5. import {Client} from 'sentry/api';
  6. import TransactionsList from 'sentry/components/discover/transactionsList';
  7. import {t} from 'sentry/locale';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  10. import {OrganizationContext} from 'sentry/views/organizationContext';
  11. const WrapperComponent = props => {
  12. return (
  13. <OrganizationContext.Provider value={props.organization}>
  14. <MEPSettingProvider _isMEPEnabled={false}>
  15. <TransactionsList {...props} />
  16. </MEPSettingProvider>
  17. </OrganizationContext.Provider>
  18. );
  19. };
  20. describe('TransactionsList', function () {
  21. let wrapper;
  22. let api;
  23. let location;
  24. let context;
  25. let organization;
  26. let project;
  27. let eventView;
  28. let options;
  29. let handleDropdownChange;
  30. const initialize = (config = {}) => {
  31. context = initializeOrg(config);
  32. organization = context.organization;
  33. project = context.project;
  34. };
  35. beforeEach(function () {
  36. api = new Client();
  37. location = {
  38. pathname: '/',
  39. query: {},
  40. };
  41. handleDropdownChange = value => {
  42. const selected = options.find(option => option.value === value);
  43. if (selected) {
  44. wrapper.setProps({selected});
  45. }
  46. };
  47. });
  48. describe('Basic', function () {
  49. let generateLink;
  50. beforeEach(function () {
  51. initialize();
  52. eventView = EventView.fromSavedQuery({
  53. id: '',
  54. name: 'test query',
  55. version: 2,
  56. fields: ['transaction', 'count()'],
  57. projects: [project.id],
  58. });
  59. options = [
  60. {
  61. sort: {kind: 'asc', field: 'transaction'},
  62. value: 'name',
  63. label: t('Transactions'),
  64. },
  65. {
  66. sort: {kind: 'desc', field: 'count'},
  67. value: 'count',
  68. label: t('Failing Transactions'),
  69. },
  70. ];
  71. generateLink = {
  72. transaction: (org, row, query) => ({
  73. pathname: `/${org.slug}`,
  74. query: {
  75. ...query,
  76. transaction: row.transaction,
  77. count: row.count,
  78. 'count()': row['count()'],
  79. },
  80. }),
  81. };
  82. MockApiClient.addMockResponse({
  83. url: `/organizations/${organization.slug}/eventsv2/`,
  84. body: {
  85. meta: {transaction: 'string', count: 'number'},
  86. data: [
  87. {transaction: '/a', count: 100},
  88. {transaction: '/b', count: 1000},
  89. ],
  90. },
  91. match: [MockApiClient.matchQuery({sort: 'transaction'})],
  92. });
  93. MockApiClient.addMockResponse({
  94. url: `/organizations/${organization.slug}/eventsv2/`,
  95. body: {
  96. meta: {transaction: 'string', count: 'number'},
  97. data: [
  98. {transaction: '/b', count: 1000},
  99. {transaction: '/a', count: 100},
  100. ],
  101. },
  102. match: [MockApiClient.matchQuery({sort: '-count'})],
  103. });
  104. MockApiClient.addMockResponse({
  105. url: `/organizations/${organization.slug}/events/`,
  106. body: {
  107. meta: {fields: {transaction: 'string', 'count()': 'number'}},
  108. data: [
  109. {transaction: '/a', 'count()': 100},
  110. {transaction: '/b', 'count()': 1000},
  111. ],
  112. },
  113. match: [MockApiClient.matchQuery({sort: 'transaction'})],
  114. });
  115. MockApiClient.addMockResponse({
  116. url: `/organizations/${organization.slug}/events/`,
  117. body: {
  118. meta: {fields: {transaction: 'string', 'count()': 'number'}},
  119. data: [
  120. {transaction: '/b', 'count()': 1000},
  121. {transaction: '/a', 'count()': 100},
  122. ],
  123. },
  124. match: [MockApiClient.matchQuery({sort: '-count'})],
  125. });
  126. MockApiClient.addMockResponse({
  127. url: `/organizations/${organization.slug}/events-trends/`,
  128. body: {
  129. meta: {
  130. transaction: 'string',
  131. trend_percentage: 'percentage',
  132. trend_difference: 'number',
  133. },
  134. data: [
  135. {transaction: '/a', 'trend_percentage()': 1.25, 'trend_difference()': 25},
  136. {transaction: '/b', 'trend_percentage()': 1.05, 'trend_difference()': 5},
  137. ],
  138. },
  139. });
  140. });
  141. const openDropdown = async w => {
  142. await act(async () => {
  143. triggerPress(w.find('CompactSelect Button'));
  144. await tick();
  145. w.update();
  146. });
  147. };
  148. const selectDropdownOption = async (w, selection) => {
  149. await openDropdown(w);
  150. await act(async () => {
  151. triggerPress(w.find(`MenuItemWrap[value="${selection}"]`));
  152. await tick();
  153. w.update();
  154. });
  155. };
  156. describe('with eventsv2', function () {
  157. it('renders basic UI components', async function () {
  158. wrapper = mountWithTheme(
  159. <WrapperComponent
  160. api={api}
  161. location={location}
  162. organization={organization}
  163. eventView={eventView}
  164. selected={options[0]}
  165. options={options}
  166. handleDropdownChange={handleDropdownChange}
  167. />
  168. );
  169. await tick();
  170. wrapper.update();
  171. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  172. await openDropdown(wrapper);
  173. expect(wrapper.find('MenuItemWrap')).toHaveLength(2);
  174. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  175. expect(wrapper.find('Pagination')).toHaveLength(1);
  176. expect(wrapper.find('PanelTable')).toHaveLength(1);
  177. // 2 for the transaction names
  178. expect(wrapper.find('GridCell')).toHaveLength(2);
  179. // 2 for the counts
  180. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  181. });
  182. it('renders a trend view', async function () {
  183. options.push({
  184. sort: {kind: 'desc', field: 'trend_percentage()'},
  185. value: 'regression',
  186. label: t('Trending Regressions'),
  187. trendType: 'regression',
  188. });
  189. wrapper = mountWithTheme(
  190. <WrapperComponent
  191. api={api}
  192. location={location}
  193. organization={organization}
  194. trendView={eventView}
  195. selected={options[2]}
  196. options={options}
  197. handleDropdownChange={handleDropdownChange}
  198. />
  199. );
  200. await tick();
  201. wrapper.update();
  202. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  203. await openDropdown(wrapper);
  204. expect(wrapper.find('MenuItemWrap')).toHaveLength(3);
  205. expect(wrapper.find('DiscoverButton')).toHaveLength(0);
  206. expect(wrapper.find('Pagination')).toHaveLength(1);
  207. expect(wrapper.find('PanelTable')).toHaveLength(1);
  208. // trend_percentage and transaction name
  209. expect(wrapper.find('GridCell')).toHaveLength(4);
  210. // trend_difference
  211. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  212. });
  213. it('renders default titles', async function () {
  214. wrapper = mountWithTheme(
  215. <WrapperComponent
  216. api={api}
  217. location={location}
  218. organization={organization}
  219. eventView={eventView}
  220. selected={options[0]}
  221. options={options}
  222. handleDropdownChange={handleDropdownChange}
  223. />
  224. );
  225. await tick();
  226. wrapper.update();
  227. const headers = wrapper.find('SortLink');
  228. expect(headers).toHaveLength(2);
  229. expect(headers.first().text()).toEqual('transaction');
  230. expect(headers.last().text()).toEqual('count()');
  231. });
  232. it('renders custom titles', async function () {
  233. wrapper = mountWithTheme(
  234. <WrapperComponent
  235. api={api}
  236. location={location}
  237. organization={organization}
  238. eventView={eventView}
  239. selected={options[0]}
  240. options={options}
  241. handleDropdownChange={handleDropdownChange}
  242. titles={['foo', 'bar']}
  243. />
  244. );
  245. await tick();
  246. wrapper.update();
  247. const headers = wrapper.find('SortLink');
  248. expect(headers).toHaveLength(2);
  249. expect(headers.first().text()).toEqual('foo');
  250. expect(headers.last().text()).toEqual('bar');
  251. });
  252. it('allows users to change the sort in the dropdown', async function () {
  253. wrapper = mountWithTheme(
  254. <WrapperComponent
  255. api={api}
  256. location={location}
  257. organization={organization}
  258. eventView={eventView}
  259. selected={options[0]}
  260. options={options}
  261. handleDropdownChange={handleDropdownChange}
  262. />
  263. );
  264. await tick();
  265. wrapper.update();
  266. // initial sort is ascending by transaction name
  267. expect(wrapper.find('GridCell').first().text()).toEqual('/a');
  268. expect(wrapper.find('GridCellNumber').first().text()).toEqual('100');
  269. expect(wrapper.find('GridCell').last().text()).toEqual('/b');
  270. expect(wrapper.find('GridCellNumber').last().text()).toEqual('1000');
  271. await selectDropdownOption(wrapper, 'count');
  272. await tick();
  273. wrapper.update();
  274. // now the sort is descending by count
  275. expect(wrapper.find('GridCell').first().text()).toEqual('/b');
  276. expect(wrapper.find('GridCellNumber').first().text()).toEqual('1000');
  277. expect(wrapper.find('GridCell').last().text()).toEqual('/a');
  278. expect(wrapper.find('GridCellNumber').last().text()).toEqual('100');
  279. });
  280. it('generates link for the transaction cell', async function () {
  281. wrapper = mountWithTheme(
  282. <WrapperComponent
  283. api={api}
  284. location={location}
  285. organization={organization}
  286. eventView={eventView}
  287. selected={options[0]}
  288. options={options}
  289. handleDropdownChange={handleDropdownChange}
  290. generateLink={generateLink}
  291. />
  292. );
  293. await tick();
  294. wrapper.update();
  295. const links = wrapper.find('Link');
  296. expect(links).toHaveLength(2);
  297. expect(links.first().props().to).toEqual(
  298. expect.objectContaining({
  299. pathname: `/${organization.slug}`,
  300. query: {
  301. transaction: '/a',
  302. count: 100,
  303. },
  304. })
  305. );
  306. expect(links.last().props().to).toEqual(
  307. expect.objectContaining({
  308. pathname: `/${organization.slug}`,
  309. query: {
  310. transaction: '/b',
  311. count: 1000,
  312. },
  313. })
  314. );
  315. });
  316. it('handles forceLoading correctly', async function () {
  317. wrapper = mountWithTheme(
  318. <WrapperComponent
  319. api={null}
  320. location={location}
  321. organization={organization}
  322. eventView={eventView}
  323. selected={options[0]}
  324. options={options}
  325. handleDropdownChange={handleDropdownChange}
  326. forceLoading
  327. />
  328. );
  329. expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
  330. wrapper.setProps({api, forceLoading: false});
  331. await tick();
  332. wrapper.update();
  333. expect(wrapper.find('LoadingIndicator')).toHaveLength(0);
  334. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  335. await openDropdown(wrapper);
  336. expect(wrapper.find('MenuItemWrap')).toHaveLength(2);
  337. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  338. expect(wrapper.find('Pagination')).toHaveLength(1);
  339. expect(wrapper.find('PanelTable')).toHaveLength(1);
  340. // 2 for the transaction names
  341. expect(wrapper.find('GridCell')).toHaveLength(2);
  342. // 2 for the counts
  343. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  344. });
  345. });
  346. describe('with events', function () {
  347. beforeEach(function () {
  348. organization.features.push('performance-frontend-use-events-endpoint');
  349. });
  350. it('renders basic UI components', async function () {
  351. wrapper = mountWithTheme(
  352. <WrapperComponent
  353. api={api}
  354. location={location}
  355. organization={organization}
  356. eventView={eventView}
  357. selected={options[0]}
  358. options={options}
  359. handleDropdownChange={handleDropdownChange}
  360. />
  361. );
  362. await tick();
  363. wrapper.update();
  364. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  365. await openDropdown(wrapper);
  366. expect(wrapper.find('MenuItemWrap')).toHaveLength(2);
  367. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  368. expect(wrapper.find('Pagination')).toHaveLength(1);
  369. expect(wrapper.find('PanelTable')).toHaveLength(1);
  370. // 2 for the transaction names
  371. expect(wrapper.find('GridCell')).toHaveLength(2);
  372. // 2 for the counts
  373. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  374. });
  375. it('renders a trend view', async function () {
  376. options.push({
  377. sort: {kind: 'desc', field: 'trend_percentage()'},
  378. value: 'regression',
  379. label: t('Trending Regressions'),
  380. trendType: 'regression',
  381. });
  382. wrapper = mountWithTheme(
  383. <WrapperComponent
  384. api={api}
  385. location={location}
  386. organization={organization}
  387. trendView={eventView}
  388. selected={options[2]}
  389. options={options}
  390. handleDropdownChange={handleDropdownChange}
  391. />
  392. );
  393. await tick();
  394. wrapper.update();
  395. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  396. await openDropdown(wrapper);
  397. expect(wrapper.find('MenuItemWrap')).toHaveLength(3);
  398. expect(wrapper.find('DiscoverButton')).toHaveLength(0);
  399. expect(wrapper.find('Pagination')).toHaveLength(1);
  400. expect(wrapper.find('PanelTable')).toHaveLength(1);
  401. // trend_percentage and transaction name
  402. expect(wrapper.find('GridCell')).toHaveLength(4);
  403. // trend_difference
  404. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  405. });
  406. it('renders default titles', async function () {
  407. wrapper = mountWithTheme(
  408. <WrapperComponent
  409. api={api}
  410. location={location}
  411. organization={organization}
  412. eventView={eventView}
  413. selected={options[0]}
  414. options={options}
  415. handleDropdownChange={handleDropdownChange}
  416. />
  417. );
  418. await tick();
  419. wrapper.update();
  420. const headers = wrapper.find('SortLink');
  421. expect(headers).toHaveLength(2);
  422. expect(headers.first().text()).toEqual('transaction');
  423. expect(headers.last().text()).toEqual('count()');
  424. });
  425. it('renders custom titles', async function () {
  426. wrapper = mountWithTheme(
  427. <WrapperComponent
  428. api={api}
  429. location={location}
  430. organization={organization}
  431. eventView={eventView}
  432. selected={options[0]}
  433. options={options}
  434. handleDropdownChange={handleDropdownChange}
  435. titles={['foo', 'bar']}
  436. />
  437. );
  438. await tick();
  439. wrapper.update();
  440. const headers = wrapper.find('SortLink');
  441. expect(headers).toHaveLength(2);
  442. expect(headers.first().text()).toEqual('foo');
  443. expect(headers.last().text()).toEqual('bar');
  444. });
  445. it('allows users to change the sort in the dropdown', async function () {
  446. wrapper = mountWithTheme(
  447. <WrapperComponent
  448. api={api}
  449. location={location}
  450. organization={organization}
  451. eventView={eventView}
  452. selected={options[0]}
  453. options={options}
  454. handleDropdownChange={handleDropdownChange}
  455. />
  456. );
  457. await tick();
  458. wrapper.update();
  459. // initial sort is ascending by transaction name
  460. expect(wrapper.find('GridCell').first().text()).toEqual('/a');
  461. expect(wrapper.find('GridCellNumber').first().text()).toEqual('100');
  462. expect(wrapper.find('GridCell').last().text()).toEqual('/b');
  463. expect(wrapper.find('GridCellNumber').last().text()).toEqual('1000');
  464. await selectDropdownOption(wrapper, 'count');
  465. await tick();
  466. wrapper.update();
  467. // now the sort is descending by count
  468. expect(wrapper.find('GridCell').first().text()).toEqual('/b');
  469. expect(wrapper.find('GridCellNumber').first().text()).toEqual('1000');
  470. expect(wrapper.find('GridCell').last().text()).toEqual('/a');
  471. expect(wrapper.find('GridCellNumber').last().text()).toEqual('100');
  472. });
  473. it('generates link for the transaction cell', async function () {
  474. wrapper = mountWithTheme(
  475. <WrapperComponent
  476. api={api}
  477. location={location}
  478. organization={organization}
  479. eventView={eventView}
  480. selected={options[0]}
  481. options={options}
  482. handleDropdownChange={handleDropdownChange}
  483. generateLink={generateLink}
  484. />
  485. );
  486. await tick();
  487. wrapper.update();
  488. const links = wrapper.find('Link');
  489. expect(links).toHaveLength(2);
  490. expect(links.first().props().to).toEqual(
  491. expect.objectContaining({
  492. pathname: `/${organization.slug}`,
  493. query: {
  494. transaction: '/a',
  495. 'count()': 100,
  496. },
  497. })
  498. );
  499. expect(links.last().props().to).toEqual(
  500. expect.objectContaining({
  501. pathname: `/${organization.slug}`,
  502. query: {
  503. transaction: '/b',
  504. 'count()': 1000,
  505. },
  506. })
  507. );
  508. });
  509. it('handles forceLoading correctly', async function () {
  510. wrapper = mountWithTheme(
  511. <WrapperComponent
  512. api={null}
  513. location={location}
  514. organization={organization}
  515. eventView={eventView}
  516. selected={options[0]}
  517. options={options}
  518. handleDropdownChange={handleDropdownChange}
  519. forceLoading
  520. />
  521. );
  522. expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
  523. wrapper.setProps({api, forceLoading: false});
  524. await tick();
  525. wrapper.update();
  526. expect(wrapper.find('LoadingIndicator')).toHaveLength(0);
  527. expect(wrapper.find('CompactSelect')).toHaveLength(1);
  528. await openDropdown(wrapper);
  529. expect(wrapper.find('MenuItemWrap')).toHaveLength(2);
  530. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  531. expect(wrapper.find('Pagination')).toHaveLength(1);
  532. expect(wrapper.find('PanelTable')).toHaveLength(1);
  533. // 2 for the transaction names
  534. expect(wrapper.find('GridCell')).toHaveLength(2);
  535. // 2 for the counts
  536. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  537. });
  538. });
  539. });
  540. });