transactionsList.spec.tsx 13 KB

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