transactionsList.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import TransactionsList from 'sentry/components/discover/transactionsList';
  4. import {t} from 'sentry/locale';
  5. import EventView from 'sentry/utils/discover/eventView';
  6. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  7. import {OrganizationContext} from 'sentry/views/organizationContext';
  8. function WrapperComponent(props) {
  9. return (
  10. <OrganizationContext.Provider value={props.organization}>
  11. <MEPSettingProvider _isMEPEnabled={false}>
  12. <TransactionsList {...props} />
  13. </MEPSettingProvider>
  14. </OrganizationContext.Provider>
  15. );
  16. }
  17. describe('TransactionsList', function () {
  18. let api;
  19. let location;
  20. let context;
  21. let organization;
  22. let project;
  23. let eventView;
  24. let options;
  25. let handleDropdownChange;
  26. const initialize = (config = {}) => {
  27. context = initializeOrg(config);
  28. organization = context.organization;
  29. project = context.project;
  30. };
  31. beforeEach(function () {
  32. location = {
  33. pathname: '/',
  34. query: {},
  35. };
  36. handleDropdownChange = () => {
  37. //
  38. };
  39. });
  40. describe('Basic', function () {
  41. let generateLink, routerContext;
  42. beforeEach(function () {
  43. routerContext = TestStubs.routerContext([{organization}]);
  44. initialize();
  45. eventView = EventView.fromSavedQuery({
  46. id: '',
  47. name: 'test query',
  48. version: 2,
  49. fields: ['transaction', 'count()'],
  50. projects: [project.id],
  51. });
  52. options = [
  53. {
  54. sort: {kind: 'asc', field: 'transaction'},
  55. value: 'name',
  56. label: t('Transactions'),
  57. },
  58. {
  59. sort: {kind: 'desc', field: 'count'},
  60. value: 'count',
  61. label: t('Failing Transactions'),
  62. },
  63. ];
  64. generateLink = {
  65. transaction: (org, row, query) => ({
  66. pathname: `/${org.slug}`,
  67. query: {
  68. ...query,
  69. transaction: row.transaction,
  70. count: row.count,
  71. 'count()': row['count()'],
  72. },
  73. }),
  74. };
  75. const pageLinks =
  76. '<https://sentry.io/fake/previous>; rel="previous"; results="false"; cursor="0:0:1", ' +
  77. '<https://sentry.io/fake/next>; rel="next"; results="true"; cursor="0:20:0"';
  78. MockApiClient.addMockResponse({
  79. url: `/organizations/${organization.slug}/events/`,
  80. headers: {Link: pageLinks},
  81. body: {
  82. meta: {transaction: 'string', count: 'number'},
  83. data: [
  84. {transaction: '/a', count: 100},
  85. {transaction: '/b', count: 1000},
  86. ],
  87. },
  88. match: [MockApiClient.matchQuery({sort: 'transaction'})],
  89. });
  90. MockApiClient.addMockResponse({
  91. url: `/organizations/${organization.slug}/events/`,
  92. headers: {Link: pageLinks},
  93. body: {
  94. meta: {transaction: 'string', count: 'number'},
  95. data: [
  96. {transaction: '/b', count: 1000},
  97. {transaction: '/a', count: 100},
  98. ],
  99. },
  100. match: [MockApiClient.matchQuery({sort: '-count'})],
  101. });
  102. MockApiClient.addMockResponse({
  103. url: `/organizations/${organization.slug}/events/`,
  104. headers: {Link: pageLinks},
  105. body: {
  106. meta: {fields: {transaction: 'string', 'count()': 'number'}},
  107. data: [
  108. {transaction: '/a', 'count()': 100},
  109. {transaction: '/b', 'count()': 1000},
  110. ],
  111. },
  112. match: [MockApiClient.matchQuery({sort: 'transaction'})],
  113. });
  114. MockApiClient.addMockResponse({
  115. url: `/organizations/${organization.slug}/events/`,
  116. headers: {Link: pageLinks},
  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. headers: {Link: pageLinks},
  129. body: {
  130. meta: {
  131. transaction: 'string',
  132. trend_percentage: 'percentage',
  133. trend_difference: 'number',
  134. },
  135. data: [
  136. {transaction: '/a', 'trend_percentage()': 1.25, 'trend_difference()': 25},
  137. {transaction: '/b', 'trend_percentage()': 1.05, 'trend_difference()': 5},
  138. ],
  139. },
  140. });
  141. });
  142. it('renders basic UI components', async function () {
  143. render(
  144. <WrapperComponent
  145. api={api}
  146. location={location}
  147. organization={organization}
  148. eventView={eventView}
  149. selected={options[0]}
  150. options={options}
  151. handleDropdownChange={handleDropdownChange}
  152. />,
  153. {
  154. context: routerContext,
  155. }
  156. );
  157. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  158. expect(
  159. screen.getByRole('button', {
  160. name: 'Open in Discover',
  161. })
  162. ).toBeInTheDocument();
  163. expect(screen.getAllByTestId('table-header')).toHaveLength(2);
  164. expect(
  165. screen.getByRole('button', {name: 'Filter Transactions'})
  166. ).toBeInTheDocument();
  167. expect(screen.getByRole('button', {name: 'Previous'})).toBeInTheDocument();
  168. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  169. const gridCells = screen.getAllByTestId('grid-cell');
  170. expect(gridCells.map(e => e.textContent)).toEqual(['/a', '100', '/b', '1000']);
  171. });
  172. it('renders a trend view', async function () {
  173. options.push({
  174. sort: {kind: 'desc', field: 'trend_percentage()'},
  175. value: 'regression',
  176. label: t('Trending Regressions'),
  177. trendType: 'regression',
  178. });
  179. render(
  180. <WrapperComponent
  181. api={api}
  182. location={location}
  183. organization={organization}
  184. trendView={eventView}
  185. selected={options[2]}
  186. options={options}
  187. handleDropdownChange={handleDropdownChange}
  188. />,
  189. {
  190. context: routerContext,
  191. }
  192. );
  193. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  194. const filterDropdown = screen.getByRole('button', {
  195. name: 'Filter Trending Regressions',
  196. });
  197. expect(filterDropdown).toBeInTheDocument();
  198. await userEvent.click(filterDropdown);
  199. const menuOptions = await screen.findAllByRole('option');
  200. expect(menuOptions.map(e => e.textContent)).toEqual([
  201. 'Transactions',
  202. 'Failing Transactions',
  203. 'Trending Regressions',
  204. ]);
  205. expect(
  206. screen.queryByRole('button', {
  207. name: 'Open in Discover',
  208. })
  209. ).not.toBeInTheDocument();
  210. expect(screen.getByRole('button', {name: 'Previous'})).toBeInTheDocument();
  211. expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument();
  212. const gridCells = screen.getAllByTestId('grid-cell');
  213. expect(gridCells.map(e => e.textContent)).toEqual(
  214. expect.arrayContaining([
  215. '/a',
  216. '(no value)',
  217. '(no value)',
  218. '/b',
  219. '(no value)',
  220. '(no value)',
  221. ])
  222. );
  223. const tableHeadings = screen.getAllByTestId('table-header');
  224. expect(tableHeadings.map(e => e.textContent)).toEqual([
  225. 'transaction',
  226. 'percentage',
  227. 'difference',
  228. ]);
  229. });
  230. it('renders default titles', async function () {
  231. render(
  232. <WrapperComponent
  233. api={api}
  234. location={location}
  235. organization={organization}
  236. eventView={eventView}
  237. selected={options[0]}
  238. options={options}
  239. handleDropdownChange={handleDropdownChange}
  240. />,
  241. {
  242. context: routerContext,
  243. }
  244. );
  245. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  246. const tableHeadings = screen.getAllByTestId('table-header');
  247. expect(tableHeadings.map(e => e.textContent)).toEqual(['transaction', 'count()']);
  248. });
  249. it('renders custom titles', async function () {
  250. render(
  251. <WrapperComponent
  252. api={api}
  253. location={location}
  254. organization={organization}
  255. eventView={eventView}
  256. selected={options[0]}
  257. options={options}
  258. handleDropdownChange={handleDropdownChange}
  259. titles={['foo', 'bar']}
  260. />,
  261. {
  262. context: routerContext,
  263. }
  264. );
  265. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  266. const tableHeadings = screen.getAllByTestId('table-header');
  267. expect(tableHeadings.map(e => e.textContent)).toEqual(['foo', 'bar']);
  268. });
  269. it('allows users to change the sort in the dropdown', async function () {
  270. let component = null;
  271. const handleDropdown = value => {
  272. const selected = options.find(option => option.value === value);
  273. if (selected && component) {
  274. component.rerender(
  275. <WrapperComponent
  276. selected={selected}
  277. api={api}
  278. location={location}
  279. organization={organization}
  280. eventView={eventView}
  281. options={options}
  282. />
  283. );
  284. }
  285. };
  286. component = render(
  287. <WrapperComponent
  288. api={api}
  289. location={location}
  290. organization={organization}
  291. eventView={eventView}
  292. selected={options[0]}
  293. options={options}
  294. handleDropdownChange={handleDropdown}
  295. />,
  296. {
  297. context: routerContext,
  298. }
  299. );
  300. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  301. const gridCells = screen.getAllByTestId('grid-cell');
  302. expect(gridCells.map(e => e.textContent)).toEqual(['/a', '100', '/b', '1000']);
  303. const filterDropdown = screen.getByRole('button', {
  304. name: 'Filter Transactions',
  305. });
  306. expect(filterDropdown).toBeInTheDocument();
  307. await userEvent.click(filterDropdown);
  308. const menuOptions = await screen.findAllByRole('option');
  309. expect(menuOptions.map(e => e.textContent)).toEqual([
  310. 'Transactions',
  311. 'Failing Transactions',
  312. ]);
  313. await userEvent.click(menuOptions[1]); // Failing transactions is 'count' as per the test options
  314. waitFor(() => {
  315. // now the sort is descending by count
  316. expect(screen.getAllByTestId('grid-cell').map(e => e.textContent)).toEqual([
  317. '/a',
  318. '100',
  319. '/b',
  320. '1000',
  321. ]);
  322. });
  323. });
  324. it('generates link for the transaction cell', async function () {
  325. render(
  326. <WrapperComponent
  327. api={api}
  328. location={location}
  329. organization={organization}
  330. eventView={eventView}
  331. selected={options[0]}
  332. options={options}
  333. handleDropdownChange={handleDropdownChange}
  334. generateLink={generateLink}
  335. />,
  336. {context: routerContext}
  337. );
  338. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  339. const links = screen.getAllByRole('link');
  340. expect(links).toHaveLength(2);
  341. expect(links[0]).toHaveAttribute(
  342. 'href',
  343. '/org-slug?count%28%29=100&transaction=%2Fa'
  344. );
  345. expect(links[1]).toHaveAttribute(
  346. 'href',
  347. '/org-slug?count%28%29=1000&transaction=%2Fb'
  348. );
  349. });
  350. it('handles forceLoading correctly', async function () {
  351. const component = render(
  352. <WrapperComponent
  353. api={null}
  354. location={location}
  355. organization={organization}
  356. eventView={eventView}
  357. selected={options[0]}
  358. options={options}
  359. handleDropdownChange={handleDropdownChange}
  360. forceLoading
  361. />,
  362. {context: routerContext}
  363. );
  364. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  365. component.rerender(
  366. <WrapperComponent
  367. api={null}
  368. location={location}
  369. organization={organization}
  370. eventView={eventView}
  371. selected={options[0]}
  372. options={options}
  373. handleDropdownChange={handleDropdownChange}
  374. />,
  375. {context: routerContext}
  376. );
  377. expect(await screen.findByTestId('transactions-table')).toBeInTheDocument();
  378. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  379. const gridCells = screen.getAllByTestId('grid-cell');
  380. expect(gridCells.map(e => e.textContent)).toEqual(['/a', '100', '/b', '1000']);
  381. });
  382. });
  383. });