transactionsList.spec.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {Client} from 'app/api';
  4. import TransactionsList from 'app/components/discover/transactionsList';
  5. import {t} from 'app/locale';
  6. import EventView from 'app/utils/discover/eventView';
  7. describe('TransactionsList', function () {
  8. let wrapper;
  9. let api;
  10. let location;
  11. let context;
  12. let organization;
  13. let project;
  14. let eventView;
  15. let options;
  16. let handleDropdownChange;
  17. const initialize = (config = {}) => {
  18. context = initializeOrg(config);
  19. organization = context.organization;
  20. project = context.project;
  21. };
  22. beforeEach(function () {
  23. api = new Client();
  24. location = {
  25. pathname: '/',
  26. query: {},
  27. };
  28. handleDropdownChange = value => {
  29. const selected = options.find(option => option.value === value);
  30. if (selected) {
  31. wrapper.setProps({selected});
  32. }
  33. };
  34. });
  35. describe('Basic', function () {
  36. let generateLink;
  37. beforeEach(function () {
  38. initialize();
  39. eventView = EventView.fromSavedQuery({
  40. id: '',
  41. name: 'test query',
  42. version: 2,
  43. fields: ['transaction', 'count()'],
  44. projects: [project.id],
  45. });
  46. options = [
  47. {
  48. sort: {kind: 'asc', field: 'transaction'},
  49. value: 'name',
  50. label: t('Transactions'),
  51. },
  52. {
  53. sort: {kind: 'desc', field: 'count'},
  54. value: 'count',
  55. label: t('Failing Transactions'),
  56. },
  57. ];
  58. generateLink = {
  59. transaction: (org, row, query) => ({
  60. pathname: `/${org.slug}`,
  61. query: {
  62. ...query,
  63. transaction: row.transaction,
  64. count: row.count,
  65. },
  66. }),
  67. };
  68. MockApiClient.addMockResponse(
  69. {
  70. url: `/organizations/${organization.slug}/eventsv2/`,
  71. body: {
  72. meta: {transaction: 'string', count: 'number'},
  73. data: [
  74. {transaction: '/a', count: 100},
  75. {transaction: '/b', count: 1000},
  76. ],
  77. },
  78. },
  79. {
  80. predicate: (_, opts) => opts?.query?.sort === 'transaction',
  81. }
  82. );
  83. MockApiClient.addMockResponse(
  84. {
  85. url: `/organizations/${organization.slug}/eventsv2/`,
  86. body: {
  87. meta: {transaction: 'string', count: 'number'},
  88. data: [
  89. {transaction: '/b', count: 1000},
  90. {transaction: '/a', count: 100},
  91. ],
  92. },
  93. },
  94. {
  95. predicate: (_, opts) => opts?.query?.sort === '-count',
  96. }
  97. );
  98. MockApiClient.addMockResponse({
  99. url: `/organizations/${organization.slug}/events-trends/`,
  100. body: {
  101. meta: {
  102. transaction: 'string',
  103. trend_percentage: 'percentage',
  104. trend_difference: 'number',
  105. },
  106. data: [
  107. {transaction: '/a', 'trend_percentage()': 1.25, 'trend_difference()': 25},
  108. {transaction: '/b', 'trend_percentage()': 1.05, 'trend_difference()': 5},
  109. ],
  110. },
  111. });
  112. });
  113. const selectDropdownOption = (w, selection) => {
  114. w.find('DropdownControl').first().simulate('click');
  115. w.find(`DropdownItem[data-test-id="option-${selection}"] span`).simulate('click');
  116. };
  117. it('renders basic UI components', async function () {
  118. wrapper = mountWithTheme(
  119. <TransactionsList
  120. api={api}
  121. location={location}
  122. organization={organization}
  123. eventView={eventView}
  124. selected={options[0]}
  125. options={options}
  126. handleDropdownChange={handleDropdownChange}
  127. />
  128. );
  129. await tick();
  130. wrapper.update();
  131. expect(wrapper.find('DropdownControl')).toHaveLength(1);
  132. expect(wrapper.find('DropdownItem')).toHaveLength(2);
  133. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  134. expect(wrapper.find('Pagination')).toHaveLength(1);
  135. expect(wrapper.find('PanelTable')).toHaveLength(1);
  136. // 2 for the transaction names
  137. expect(wrapper.find('GridCell')).toHaveLength(2);
  138. // 2 for the counts
  139. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  140. });
  141. it('renders a trend view', async function () {
  142. options.push({
  143. sort: {kind: 'desc', field: 'trend_percentage()'},
  144. value: 'regression',
  145. label: t('Trending Regressions'),
  146. trendType: 'regression',
  147. });
  148. wrapper = mountWithTheme(
  149. <TransactionsList
  150. api={api}
  151. location={location}
  152. organization={organization}
  153. trendView={eventView}
  154. selected={options[2]}
  155. options={options}
  156. handleDropdownChange={handleDropdownChange}
  157. />
  158. );
  159. await tick();
  160. wrapper.update();
  161. expect(wrapper.find('DropdownControl')).toHaveLength(1);
  162. expect(wrapper.find('DropdownItem')).toHaveLength(3);
  163. expect(wrapper.find('DiscoverButton')).toHaveLength(0);
  164. expect(wrapper.find('Pagination')).toHaveLength(1);
  165. expect(wrapper.find('PanelTable')).toHaveLength(1);
  166. // trend_percentage and transaction name
  167. expect(wrapper.find('GridCell')).toHaveLength(4);
  168. // trend_difference
  169. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  170. });
  171. it('renders default titles', async function () {
  172. wrapper = mountWithTheme(
  173. <TransactionsList
  174. api={api}
  175. location={location}
  176. organization={organization}
  177. eventView={eventView}
  178. selected={options[0]}
  179. options={options}
  180. handleDropdownChange={handleDropdownChange}
  181. />
  182. );
  183. await tick();
  184. wrapper.update();
  185. const headers = wrapper.find('SortLink');
  186. expect(headers).toHaveLength(2);
  187. expect(headers.first().text()).toEqual('transaction');
  188. expect(headers.last().text()).toEqual('count()');
  189. });
  190. it('renders custom titles', async function () {
  191. wrapper = mountWithTheme(
  192. <TransactionsList
  193. api={api}
  194. location={location}
  195. organization={organization}
  196. eventView={eventView}
  197. selected={options[0]}
  198. options={options}
  199. handleDropdownChange={handleDropdownChange}
  200. titles={['foo', 'bar']}
  201. />
  202. );
  203. await tick();
  204. wrapper.update();
  205. const headers = wrapper.find('SortLink');
  206. expect(headers).toHaveLength(2);
  207. expect(headers.first().text()).toEqual('foo');
  208. expect(headers.last().text()).toEqual('bar');
  209. });
  210. it('allows users to change the sort in the dropdown', async function () {
  211. wrapper = mountWithTheme(
  212. <TransactionsList
  213. api={api}
  214. location={location}
  215. organization={organization}
  216. eventView={eventView}
  217. selected={options[0]}
  218. options={options}
  219. handleDropdownChange={handleDropdownChange}
  220. />
  221. );
  222. await tick();
  223. wrapper.update();
  224. // initial sort is ascending by transaction name
  225. expect(wrapper.find('GridCell').first().text()).toEqual('/a');
  226. expect(wrapper.find('GridCellNumber').first().text()).toEqual('100');
  227. expect(wrapper.find('GridCell').last().text()).toEqual('/b');
  228. expect(wrapper.find('GridCellNumber').last().text()).toEqual('1000');
  229. selectDropdownOption(wrapper, 'count');
  230. await tick();
  231. wrapper.update();
  232. // now the sort is descending by count
  233. expect(wrapper.find('GridCell').first().text()).toEqual('/b');
  234. expect(wrapper.find('GridCellNumber').first().text()).toEqual('1000');
  235. expect(wrapper.find('GridCell').last().text()).toEqual('/a');
  236. expect(wrapper.find('GridCellNumber').last().text()).toEqual('100');
  237. });
  238. it('generates link for the transaction cell', async function () {
  239. wrapper = mountWithTheme(
  240. <TransactionsList
  241. api={api}
  242. location={location}
  243. organization={organization}
  244. eventView={eventView}
  245. selected={options[0]}
  246. options={options}
  247. handleDropdownChange={handleDropdownChange}
  248. generateLink={generateLink}
  249. />
  250. );
  251. await tick();
  252. wrapper.update();
  253. const links = wrapper.find('Link');
  254. expect(links).toHaveLength(2);
  255. expect(links.first().props().to).toEqual(
  256. expect.objectContaining({
  257. pathname: `/${organization.slug}`,
  258. query: {
  259. transaction: '/a',
  260. count: 100,
  261. },
  262. })
  263. );
  264. expect(links.last().props().to).toEqual(
  265. expect.objectContaining({
  266. pathname: `/${organization.slug}`,
  267. query: {
  268. transaction: '/b',
  269. count: 1000,
  270. },
  271. })
  272. );
  273. });
  274. it('handles forceLoading correctly', async function () {
  275. wrapper = mountWithTheme(
  276. <TransactionsList
  277. api={null}
  278. location={location}
  279. organization={organization}
  280. eventView={eventView}
  281. selected={options[0]}
  282. options={options}
  283. handleDropdownChange={handleDropdownChange}
  284. forceLoading
  285. />
  286. );
  287. expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
  288. wrapper.setProps({api, forceLoading: false});
  289. await tick();
  290. wrapper.update();
  291. expect(wrapper.find('LoadingIndicator')).toHaveLength(0);
  292. expect(wrapper.find('DropdownControl')).toHaveLength(1);
  293. expect(wrapper.find('DropdownItem')).toHaveLength(2);
  294. expect(wrapper.find('DiscoverButton')).toHaveLength(1);
  295. expect(wrapper.find('Pagination')).toHaveLength(1);
  296. expect(wrapper.find('PanelTable')).toHaveLength(1);
  297. // 2 for the transaction names
  298. expect(wrapper.find('GridCell')).toHaveLength(2);
  299. // 2 for the counts
  300. expect(wrapper.find('GridCellNumber')).toHaveLength(2);
  301. });
  302. });
  303. describe('Baseline', function () {
  304. beforeEach(function () {
  305. initialize({
  306. organization: {features: 'transaction-comparison'},
  307. });
  308. eventView = EventView.fromSavedQuery({
  309. id: '',
  310. name: 'baseline query',
  311. version: 2,
  312. fields: ['id', 'transaction.duration'],
  313. projects: [project.id],
  314. });
  315. options = [
  316. {
  317. sort: {kind: 'desc', field: 'transaction.duration'},
  318. value: 'slow',
  319. label: t('Slow Transactions'),
  320. },
  321. ];
  322. MockApiClient.addMockResponse({
  323. url: `/organizations/${organization.slug}/eventsv2/`,
  324. body: {
  325. meta: {id: 'string', 'transaction.duration': 'duration'},
  326. data: [
  327. {id: 'a', 'transaction.duration': 123},
  328. {id: 'c', 'transaction.duration': 12345},
  329. ],
  330. },
  331. });
  332. MockApiClient.addMockResponse({
  333. url: `/organizations/${organization.slug}/event-baseline/`,
  334. body: {
  335. 'transaction.duration': 1234,
  336. },
  337. });
  338. });
  339. it('renders baseline comparison correctly', async function () {
  340. wrapper = mountWithTheme(
  341. <TransactionsList
  342. api={api}
  343. location={location}
  344. organization={organization}
  345. eventView={eventView}
  346. selected={options[0]}
  347. options={options}
  348. handleDropdownChange={handleDropdownChange}
  349. baseline="/"
  350. />
  351. );
  352. await tick();
  353. wrapper.update();
  354. const titles = ['id', 'transaction.duration', 'Compared to Baseline'];
  355. const headers = wrapper.find('SortLink');
  356. expect(headers).toHaveLength(titles.length);
  357. headers.forEach((header, i) => {
  358. expect(header.text()).toEqual(titles[i]);
  359. });
  360. const cellTexts = ['1.11 seconds faster', '11.11 seconds slower'];
  361. const cells = wrapper.find('BodyCellContainer[data-test-id="baseline-cell"]');
  362. expect(cells).toHaveLength(2);
  363. cells.forEach((cell, i) => {
  364. expect(cell.text()).toEqual(cellTexts[i]);
  365. });
  366. });
  367. it('renders View All Events button when provided with handler', async function () {
  368. wrapper = mountWithTheme(
  369. <TransactionsList
  370. api={api}
  371. location={location}
  372. organization={organization}
  373. eventView={eventView}
  374. selected={options[0]}
  375. options={options}
  376. handleDropdownChange={handleDropdownChange}
  377. baseline="/"
  378. handleOpenAllEventsClick={() => {}}
  379. />
  380. );
  381. await tick();
  382. wrapper.update();
  383. expect(wrapper.find('Button').last().find('span').children().html()).toEqual(
  384. 'View All Events'
  385. );
  386. });
  387. });
  388. });