table.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import {browserHistory} from 'react-router';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeData as _initializeData} from 'sentry-test/performance/initializePerformanceData';
  4. import EventView from 'sentry/utils/discover/eventView';
  5. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  6. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  7. import {OrganizationContext} from 'sentry/views/organizationContext';
  8. import Table from 'sentry/views/performance/table';
  9. const FEATURES = ['performance-view'];
  10. const initializeData = (settings = {}, features: string[] = []) => {
  11. const projects = [
  12. TestStubs.Project({id: '1', slug: '1'}),
  13. TestStubs.Project({id: '2', slug: '2'}),
  14. ];
  15. return _initializeData({
  16. features: [...FEATURES, ...features],
  17. projects,
  18. project: projects[0],
  19. ...settings,
  20. });
  21. };
  22. const WrappedComponent = ({data, ...rest}) => {
  23. return (
  24. <OrganizationContext.Provider value={data.organization}>
  25. <MEPSettingProvider>
  26. <Table
  27. organization={data.organization}
  28. location={data.router.location}
  29. setError={jest.fn()}
  30. summaryConditions=""
  31. {...data}
  32. {...rest}
  33. />
  34. </MEPSettingProvider>
  35. </OrganizationContext.Provider>
  36. );
  37. };
  38. function openContextMenu(wrapper, cellIndex) {
  39. const menu = wrapper.find('CellAction').at(cellIndex);
  40. // Hover over the menu
  41. menu.find('Container > div').at(0).simulate('mouseEnter');
  42. wrapper.update();
  43. // Open the menu
  44. wrapper.find('MenuButton').simulate('click');
  45. // Return the menu wrapper so we can interact with it.
  46. return wrapper.find('CellAction').at(cellIndex).find('Menu');
  47. }
  48. function mockEventView(data) {
  49. const eventView = new EventView({
  50. id: '1',
  51. name: 'my query',
  52. fields: [
  53. {
  54. field: 'team_key_transaction',
  55. },
  56. {
  57. field: 'transaction',
  58. },
  59. {
  60. field: 'project',
  61. },
  62. {
  63. field: 'tpm()',
  64. },
  65. {
  66. field: 'p50()',
  67. },
  68. {
  69. field: 'p95()',
  70. },
  71. {
  72. field: 'failure_rate()',
  73. },
  74. {
  75. field: 'apdex()',
  76. },
  77. {
  78. field: 'count_unique(user)',
  79. },
  80. {
  81. field: 'count_miserable(user)',
  82. },
  83. {
  84. field: 'user_misery()',
  85. },
  86. ],
  87. sorts: [{field: 'tpm ', kind: 'desc'}],
  88. query: 'event.type:transaction transaction:/api*',
  89. project: [data.projects[0].id, data.projects[1].id],
  90. start: '2019-10-01T00:00:00',
  91. end: '2019-10-02T00:00:00',
  92. statsPeriod: '14d',
  93. environment: [],
  94. additionalConditions: new MutableSearch(''),
  95. createdBy: undefined,
  96. interval: undefined,
  97. display: '',
  98. team: [],
  99. topEvents: undefined,
  100. yAxis: undefined,
  101. });
  102. return eventView;
  103. }
  104. describe('Performance > Table', function () {
  105. let eventsV2Mock, eventsMock;
  106. beforeEach(function () {
  107. browserHistory.push = jest.fn();
  108. MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/projects/',
  110. body: [],
  111. });
  112. const eventsMetaFieldsMock = {
  113. user: 'string',
  114. transaction: 'string',
  115. project: 'string',
  116. tpm: 'number',
  117. p50: 'number',
  118. p95: 'number',
  119. failure_rate: 'number',
  120. apdex: 'number',
  121. count_unique_user: 'number',
  122. count_miserable_user: 'number',
  123. user_misery: 'number',
  124. };
  125. const eventsBodyMock = [
  126. {
  127. team_key_transaction: 1,
  128. transaction: '/apple/cart',
  129. project: '2',
  130. user: 'uhoh@example.com',
  131. tpm: 30,
  132. p50: 100,
  133. p95: 500,
  134. failure_rate: 0.1,
  135. apdex: 0.6,
  136. count_unique_user: 1000,
  137. count_miserable_user: 122,
  138. user_misery: 0.114,
  139. project_threshold_config: ['duration', 300],
  140. },
  141. {
  142. team_key_transaction: 0,
  143. transaction: '/apple/checkout',
  144. project: '1',
  145. user: 'uhoh@example.com',
  146. tpm: 30,
  147. p50: 100,
  148. p95: 500,
  149. failure_rate: 0.1,
  150. apdex: 0.6,
  151. count_unique_user: 1000,
  152. count_miserable_user: 122,
  153. user_misery: 0.114,
  154. project_threshold_config: ['duration', 300],
  155. },
  156. ];
  157. eventsV2Mock = MockApiClient.addMockResponse({
  158. url: '/organizations/org-slug/eventsv2/',
  159. body: {
  160. meta: eventsMetaFieldsMock,
  161. data: eventsBodyMock,
  162. },
  163. });
  164. eventsMock = MockApiClient.addMockResponse({
  165. url: '/organizations/org-slug/events/',
  166. body: {
  167. meta: {fields: eventsMetaFieldsMock},
  168. data: eventsBodyMock,
  169. },
  170. });
  171. MockApiClient.addMockResponse({
  172. method: 'GET',
  173. url: `/organizations/org-slug/key-transactions-list/`,
  174. body: [],
  175. });
  176. });
  177. afterEach(function () {
  178. MockApiClient.clearMockResponses();
  179. });
  180. describe('with eventsv2', function () {
  181. it('renders correct cell actions without feature', async function () {
  182. const data = initializeData({
  183. query: 'event.type:transaction transaction:/api*',
  184. });
  185. const wrapper = mountWithTheme(
  186. <WrappedComponent
  187. data={data}
  188. eventView={mockEventView(data)}
  189. setError={jest.fn()}
  190. summaryConditions=""
  191. projects={data.projects}
  192. />
  193. );
  194. await tick();
  195. wrapper.update();
  196. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  197. const transactionCell = firstRow.find('GridBodyCell').at(1);
  198. expect(transactionCell.find('Link').prop('to')).toEqual({
  199. pathname: '/organizations/org-slug/performance/summary/',
  200. query: {
  201. transaction: '/apple/cart',
  202. project: '2',
  203. environment: [],
  204. statsPeriod: '14d',
  205. start: '2019-10-01T00:00:00',
  206. end: '2019-10-02T00:00:00',
  207. query: '', // drops 'transaction:/api*' and 'event.type:transaction' from the query
  208. unselectedSeries: 'p100()',
  209. showTransactions: undefined,
  210. display: undefined,
  211. trendFunction: undefined,
  212. trendColumn: undefined,
  213. },
  214. });
  215. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  216. const cellAction = userMiseryCell.find('CellAction');
  217. expect(cellAction.prop('allowActions')).toEqual([
  218. 'add',
  219. 'exclude',
  220. 'show_greater_than',
  221. 'show_less_than',
  222. 'edit_threshold',
  223. ]);
  224. const menu = openContextMenu(wrapper, 8); // User Misery Cell Action
  225. expect(menu.find('MenuButtons').find('ActionItem')).toHaveLength(3);
  226. expect(menu.find('MenuButtons').find('ActionItem').at(2).text()).toEqual(
  227. 'Edit threshold (300ms)'
  228. );
  229. });
  230. it('hides cell actions when withStaticFilters is true', async function () {
  231. const data = initializeData(
  232. {
  233. query: 'event.type:transaction transaction:/api*',
  234. },
  235. ['performance-frontend-use-events-endpoint']
  236. );
  237. const wrapper = mountWithTheme(
  238. <WrappedComponent
  239. data={data}
  240. eventView={mockEventView(data)}
  241. setError={jest.fn()}
  242. summaryConditions=""
  243. projects={data.projects}
  244. withStaticFilters
  245. />
  246. );
  247. await tick();
  248. wrapper.update();
  249. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  250. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  251. const cellAction = userMiseryCell.find('CellAction');
  252. expect(cellAction.prop('allowActions')).toEqual([]);
  253. });
  254. it('sends MEP param when setting enabled', async function () {
  255. const data = initializeData(
  256. {
  257. query: 'event.type:transaction transaction:/api*',
  258. },
  259. ['performance-use-metrics']
  260. );
  261. const wrapper = mountWithTheme(
  262. <WrappedComponent
  263. data={data}
  264. eventView={mockEventView(data)}
  265. setError={jest.fn()}
  266. summaryConditions=""
  267. projects={data.projects}
  268. isMEPEnabled
  269. />
  270. );
  271. await tick();
  272. wrapper.update();
  273. expect(eventsV2Mock).toHaveBeenCalledTimes(1);
  274. expect(eventsV2Mock).toHaveBeenNthCalledWith(
  275. 1,
  276. expect.anything(),
  277. expect.objectContaining({
  278. query: expect.objectContaining({
  279. environment: [],
  280. field: [
  281. 'team_key_transaction',
  282. 'transaction',
  283. 'project',
  284. 'tpm()',
  285. 'p50()',
  286. 'p95()',
  287. 'failure_rate()',
  288. 'apdex()',
  289. 'count_unique(user)',
  290. 'count_miserable(user)',
  291. 'user_misery()',
  292. ],
  293. metricsEnhanced: '1',
  294. preventMetricAggregates: '1',
  295. per_page: 50,
  296. project: ['1', '2'],
  297. query: 'event.type:transaction transaction:/api*',
  298. referrer: 'api.performance.landing-table',
  299. sort: '-team_key_transaction',
  300. statsPeriod: '14d',
  301. }),
  302. })
  303. );
  304. });
  305. });
  306. describe('with events', function () {
  307. it('renders correct cell actions without feature', async function () {
  308. const data = initializeData(
  309. {
  310. query: 'event.type:transaction transaction:/api*',
  311. },
  312. ['performance-frontend-use-events-endpoint']
  313. );
  314. const wrapper = mountWithTheme(
  315. <WrappedComponent
  316. data={data}
  317. eventView={mockEventView(data)}
  318. setError={jest.fn()}
  319. summaryConditions=""
  320. projects={data.projects}
  321. />
  322. );
  323. await tick();
  324. wrapper.update();
  325. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  326. const transactionCell = firstRow.find('GridBodyCell').at(1);
  327. expect(transactionCell.find('Link').prop('to')).toEqual({
  328. pathname: '/organizations/org-slug/performance/summary/',
  329. query: {
  330. transaction: '/apple/cart',
  331. project: '2',
  332. environment: [],
  333. statsPeriod: '14d',
  334. start: '2019-10-01T00:00:00',
  335. end: '2019-10-02T00:00:00',
  336. query: '', // drops 'transaction:/api*' and 'event.type:transaction' from the query
  337. unselectedSeries: 'p100()',
  338. showTransactions: undefined,
  339. display: undefined,
  340. trendFunction: undefined,
  341. trendColumn: undefined,
  342. },
  343. });
  344. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  345. const cellAction = userMiseryCell.find('CellAction');
  346. expect(cellAction.prop('allowActions')).toEqual([
  347. 'add',
  348. 'exclude',
  349. 'show_greater_than',
  350. 'show_less_than',
  351. 'edit_threshold',
  352. ]);
  353. const menu = openContextMenu(wrapper, 8); // User Misery Cell Action
  354. expect(menu.find('MenuButtons').find('ActionItem')).toHaveLength(3);
  355. expect(menu.find('MenuButtons').find('ActionItem').at(2).text()).toEqual(
  356. 'Edit threshold (300ms)'
  357. );
  358. });
  359. it('hides cell actions when withStaticFilters is true', async function () {
  360. const data = initializeData(
  361. {
  362. query: 'event.type:transaction transaction:/api*',
  363. },
  364. ['performance-frontend-use-events-endpoint']
  365. );
  366. const wrapper = mountWithTheme(
  367. <WrappedComponent
  368. data={data}
  369. eventView={mockEventView(data)}
  370. setError={jest.fn()}
  371. summaryConditions=""
  372. projects={data.projects}
  373. withStaticFilters
  374. />
  375. );
  376. await tick();
  377. wrapper.update();
  378. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  379. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  380. const cellAction = userMiseryCell.find('CellAction');
  381. expect(cellAction.prop('allowActions')).toEqual([]);
  382. });
  383. it('sends MEP param when setting enabled', async function () {
  384. const data = initializeData(
  385. {
  386. query: 'event.type:transaction transaction:/api*',
  387. },
  388. ['performance-use-metrics', 'performance-frontend-use-events-endpoint']
  389. );
  390. const wrapper = mountWithTheme(
  391. <WrappedComponent
  392. data={data}
  393. eventView={mockEventView(data)}
  394. setError={jest.fn()}
  395. summaryConditions=""
  396. projects={data.projects}
  397. isMEPEnabled
  398. />
  399. );
  400. await tick();
  401. wrapper.update();
  402. expect(eventsMock).toHaveBeenCalledTimes(1);
  403. expect(eventsMock).toHaveBeenNthCalledWith(
  404. 1,
  405. expect.anything(),
  406. expect.objectContaining({
  407. query: expect.objectContaining({
  408. environment: [],
  409. field: [
  410. 'team_key_transaction',
  411. 'transaction',
  412. 'project',
  413. 'tpm()',
  414. 'p50()',
  415. 'p95()',
  416. 'failure_rate()',
  417. 'apdex()',
  418. 'count_unique(user)',
  419. 'count_miserable(user)',
  420. 'user_misery()',
  421. ],
  422. metricsEnhanced: '1',
  423. preventMetricAggregates: '1',
  424. per_page: 50,
  425. project: ['1', '2'],
  426. query: 'event.type:transaction transaction:/api*',
  427. referrer: 'api.performance.landing-table',
  428. sort: '-team_key_transaction',
  429. statsPeriod: '14d',
  430. }),
  431. })
  432. );
  433. });
  434. });
  435. });