table.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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. selectedProject: 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. dataset: 'metrics',
  294. per_page: 50,
  295. project: ['1', '2'],
  296. query: 'event.type:transaction transaction:/api*',
  297. referrer: 'api.performance.landing-table',
  298. sort: '-team_key_transaction',
  299. statsPeriod: '14d',
  300. }),
  301. })
  302. );
  303. });
  304. });
  305. describe('with events', function () {
  306. it('renders correct cell actions without feature', async function () {
  307. const data = initializeData(
  308. {
  309. query: 'event.type:transaction transaction:/api*',
  310. },
  311. ['performance-frontend-use-events-endpoint']
  312. );
  313. const wrapper = mountWithTheme(
  314. <WrappedComponent
  315. data={data}
  316. eventView={mockEventView(data)}
  317. setError={jest.fn()}
  318. summaryConditions=""
  319. projects={data.projects}
  320. />
  321. );
  322. await tick();
  323. wrapper.update();
  324. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  325. const transactionCell = firstRow.find('GridBodyCell').at(1);
  326. expect(transactionCell.find('Link').prop('to')).toEqual({
  327. pathname: '/organizations/org-slug/performance/summary/',
  328. query: {
  329. transaction: '/apple/cart',
  330. project: '2',
  331. environment: [],
  332. statsPeriod: '14d',
  333. start: '2019-10-01T00:00:00',
  334. end: '2019-10-02T00:00:00',
  335. query: '', // drops 'transaction:/api*' and 'event.type:transaction' from the query
  336. unselectedSeries: 'p100()',
  337. showTransactions: undefined,
  338. display: undefined,
  339. trendFunction: undefined,
  340. trendColumn: undefined,
  341. },
  342. });
  343. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  344. const cellAction = userMiseryCell.find('CellAction');
  345. expect(cellAction.prop('allowActions')).toEqual([
  346. 'add',
  347. 'exclude',
  348. 'show_greater_than',
  349. 'show_less_than',
  350. 'edit_threshold',
  351. ]);
  352. const menu = openContextMenu(wrapper, 8); // User Misery Cell Action
  353. expect(menu.find('MenuButtons').find('ActionItem')).toHaveLength(3);
  354. expect(menu.find('MenuButtons').find('ActionItem').at(2).text()).toEqual(
  355. 'Edit threshold (300ms)'
  356. );
  357. });
  358. it('hides cell actions when withStaticFilters is true', async function () {
  359. const data = initializeData(
  360. {
  361. query: 'event.type:transaction transaction:/api*',
  362. },
  363. ['performance-frontend-use-events-endpoint']
  364. );
  365. const wrapper = mountWithTheme(
  366. <WrappedComponent
  367. data={data}
  368. eventView={mockEventView(data)}
  369. setError={jest.fn()}
  370. summaryConditions=""
  371. projects={data.projects}
  372. withStaticFilters
  373. />
  374. );
  375. await tick();
  376. wrapper.update();
  377. const firstRow = wrapper.find('GridBody').find('GridRow').at(0);
  378. const userMiseryCell = firstRow.find('GridBodyCell').at(9);
  379. const cellAction = userMiseryCell.find('CellAction');
  380. expect(cellAction.prop('allowActions')).toEqual([]);
  381. });
  382. it('sends MEP param when setting enabled', async function () {
  383. const data = initializeData(
  384. {
  385. query: 'event.type:transaction transaction:/api*',
  386. },
  387. ['performance-use-metrics', 'performance-frontend-use-events-endpoint']
  388. );
  389. const wrapper = mountWithTheme(
  390. <WrappedComponent
  391. data={data}
  392. eventView={mockEventView(data)}
  393. setError={jest.fn()}
  394. summaryConditions=""
  395. projects={data.projects}
  396. isMEPEnabled
  397. />
  398. );
  399. await tick();
  400. wrapper.update();
  401. expect(eventsMock).toHaveBeenCalledTimes(1);
  402. expect(eventsMock).toHaveBeenNthCalledWith(
  403. 1,
  404. expect.anything(),
  405. expect.objectContaining({
  406. query: expect.objectContaining({
  407. environment: [],
  408. field: [
  409. 'team_key_transaction',
  410. 'transaction',
  411. 'project',
  412. 'tpm()',
  413. 'p50()',
  414. 'p95()',
  415. 'failure_rate()',
  416. 'apdex()',
  417. 'count_unique(user)',
  418. 'count_miserable(user)',
  419. 'user_misery()',
  420. ],
  421. dataset: 'metrics',
  422. per_page: 50,
  423. project: ['1', '2'],
  424. query: 'event.type:transaction transaction:/api*',
  425. referrer: 'api.performance.landing-table',
  426. sort: '-team_key_transaction',
  427. statsPeriod: '14d',
  428. }),
  429. })
  430. );
  431. });
  432. });
  433. });