cellAction.spec.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  2. import EventView from 'sentry/utils/discover/eventView';
  3. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  4. import CellAction, {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction';
  5. const defaultData = {
  6. transaction: 'best-transaction',
  7. count: 19,
  8. timestamp: '2020-06-09T01:46:25+00:00',
  9. release: 'F2520C43515BD1F0E8A6BD46233324641A370BF6',
  10. nullValue: null,
  11. 'measurements.fcp': 1234,
  12. 'percentile(measurements.fcp, 0.5)': 1234,
  13. 'error.handled': [null],
  14. 'error.type': [
  15. 'ServerException',
  16. 'ClickhouseError',
  17. 'QueryException',
  18. 'QueryException',
  19. ],
  20. };
  21. function renderComponent(
  22. eventView,
  23. handleCellAction,
  24. columnIndex = 0,
  25. data = defaultData
  26. ) {
  27. return render(
  28. <CellAction
  29. dataRow={data}
  30. eventView={eventView}
  31. column={eventView.getColumns()[columnIndex]}
  32. handleCellAction={handleCellAction}
  33. >
  34. <strong>some content</strong>
  35. </CellAction>
  36. );
  37. }
  38. describe('Discover -> CellAction', function () {
  39. const location = {
  40. query: {
  41. id: '42',
  42. name: 'best query',
  43. field: [
  44. 'transaction',
  45. 'count()',
  46. 'timestamp',
  47. 'release',
  48. 'nullValue',
  49. 'measurements.fcp',
  50. 'percentile(measurements.fcp, 0.5)',
  51. 'error.handled',
  52. 'error.type',
  53. ],
  54. widths: ['437', '647', '416', '905'],
  55. sort: ['title'],
  56. query: 'event.type:transaction',
  57. project: [123],
  58. start: '2019-10-01T00:00:00',
  59. end: '2019-10-02T00:00:00',
  60. statsPeriod: '14d',
  61. environment: ['staging'],
  62. yAxis: 'p95',
  63. },
  64. };
  65. const view = EventView.fromLocation(location);
  66. function hoverContainer() {
  67. userEvent.hover(screen.getByText('some content'));
  68. }
  69. function unhoverContainer() {
  70. userEvent.unhover(screen.getByText('some content'));
  71. }
  72. function openMenu() {
  73. hoverContainer();
  74. userEvent.click(screen.getByRole('button'));
  75. }
  76. describe('hover menu button', function () {
  77. it('shows no menu by default', function () {
  78. renderComponent(view);
  79. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  80. });
  81. it('shows a menu on hover, and hides again', function () {
  82. renderComponent(view);
  83. hoverContainer();
  84. expect(screen.getByRole('button')).toBeInTheDocument();
  85. unhoverContainer();
  86. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  87. });
  88. });
  89. describe('opening the menu', function () {
  90. it('toggles the menu on click', function () {
  91. renderComponent(view);
  92. openMenu();
  93. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  94. });
  95. });
  96. describe('per cell actions', function () {
  97. let handleCellAction;
  98. beforeEach(function () {
  99. handleCellAction = jest.fn();
  100. });
  101. it('add button appends condition', function () {
  102. renderComponent(view, handleCellAction);
  103. openMenu();
  104. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  105. expect(handleCellAction).toHaveBeenCalledWith('add', 'best-transaction');
  106. });
  107. it('exclude button adds condition', function () {
  108. renderComponent(view, handleCellAction);
  109. openMenu();
  110. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  111. expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
  112. });
  113. it('exclude button appends exclusions', function () {
  114. const excludeView = EventView.fromLocation({
  115. query: {...location.query, query: '!transaction:nope'},
  116. });
  117. renderComponent(excludeView, handleCellAction);
  118. openMenu();
  119. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  120. expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
  121. });
  122. it('go to summary button goes to transaction summary page', function () {
  123. renderComponent(view, handleCellAction);
  124. openMenu();
  125. userEvent.click(screen.getByRole('button', {name: 'Go to summary'}));
  126. expect(handleCellAction).toHaveBeenCalledWith('transaction', 'best-transaction');
  127. });
  128. it('go to release button goes to release health page', function () {
  129. renderComponent(view, handleCellAction, 3);
  130. openMenu();
  131. userEvent.click(screen.getByRole('button', {name: 'Go to release'}));
  132. expect(handleCellAction).toHaveBeenCalledWith(
  133. 'release',
  134. 'F2520C43515BD1F0E8A6BD46233324641A370BF6'
  135. );
  136. });
  137. it('greater than button adds condition', function () {
  138. renderComponent(view, handleCellAction, 2);
  139. openMenu();
  140. userEvent.click(screen.getByRole('button', {name: 'Show values greater than'}));
  141. expect(handleCellAction).toHaveBeenCalledWith(
  142. 'show_greater_than',
  143. '2020-06-09T01:46:25+00:00'
  144. );
  145. });
  146. it('less than button adds condition', function () {
  147. renderComponent(view, handleCellAction, 2);
  148. openMenu();
  149. userEvent.click(screen.getByRole('button', {name: 'Show values less than'}));
  150. expect(handleCellAction).toHaveBeenCalledWith(
  151. 'show_less_than',
  152. '2020-06-09T01:46:25+00:00'
  153. );
  154. });
  155. it('error.handled with null adds condition', function () {
  156. renderComponent(view, handleCellAction, 7, defaultData);
  157. openMenu();
  158. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  159. expect(handleCellAction).toHaveBeenCalledWith('add', 1);
  160. });
  161. it('error.type with array values adds condition', function () {
  162. renderComponent(view, handleCellAction, 8, defaultData);
  163. openMenu();
  164. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  165. expect(handleCellAction).toHaveBeenCalledWith('add', [
  166. 'ServerException',
  167. 'ClickhouseError',
  168. 'QueryException',
  169. 'QueryException',
  170. ]);
  171. });
  172. it('error.handled with 0 adds condition', function () {
  173. renderComponent(view, handleCellAction, 7, {
  174. ...defaultData,
  175. 'error.handled': [0],
  176. });
  177. openMenu();
  178. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  179. expect(handleCellAction).toHaveBeenCalledWith('add', [0]);
  180. });
  181. it('show appropriate actions for string cells', function () {
  182. renderComponent(view, handleCellAction, 0);
  183. openMenu();
  184. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  185. expect(
  186. screen.getByRole('button', {name: 'Exclude from filter'})
  187. ).toBeInTheDocument();
  188. expect(
  189. screen.queryByRole('button', {name: 'Show values greater than'})
  190. ).not.toBeInTheDocument();
  191. expect(
  192. screen.queryByRole('button', {name: 'Show values less than'})
  193. ).not.toBeInTheDocument();
  194. });
  195. it('show appropriate actions for string cells with null values', function () {
  196. renderComponent(view, handleCellAction, 4);
  197. openMenu();
  198. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  199. expect(
  200. screen.getByRole('button', {name: 'Exclude from filter'})
  201. ).toBeInTheDocument();
  202. });
  203. it('show appropriate actions for number cells', function () {
  204. renderComponent(view, handleCellAction, 1);
  205. openMenu();
  206. expect(
  207. screen.queryByRole('button', {name: 'Add to filter'})
  208. ).not.toBeInTheDocument();
  209. expect(
  210. screen.queryByRole('button', {name: 'Exclude from filter'})
  211. ).not.toBeInTheDocument();
  212. expect(
  213. screen.getByRole('button', {name: 'Show values greater than'})
  214. ).toBeInTheDocument();
  215. expect(
  216. screen.getByRole('button', {name: 'Show values less than'})
  217. ).toBeInTheDocument();
  218. });
  219. it('show appropriate actions for date cells', function () {
  220. renderComponent(view, handleCellAction, 2);
  221. openMenu();
  222. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  223. expect(
  224. screen.queryByRole('button', {name: 'Exclude from filter'})
  225. ).not.toBeInTheDocument();
  226. expect(
  227. screen.getByRole('button', {name: 'Show values greater than'})
  228. ).toBeInTheDocument();
  229. expect(
  230. screen.getByRole('button', {name: 'Show values less than'})
  231. ).toBeInTheDocument();
  232. });
  233. it('show appropriate actions for release cells', function () {
  234. renderComponent(view, handleCellAction, 3);
  235. openMenu();
  236. expect(screen.getByRole('button', {name: 'Go to release'})).toBeInTheDocument();
  237. });
  238. it('show appropriate actions for empty release cells', function () {
  239. renderComponent(view, handleCellAction, 3, {...defaultData, release: null});
  240. openMenu();
  241. expect(
  242. screen.queryByRole('button', {name: 'Go to release'})
  243. ).not.toBeInTheDocument();
  244. });
  245. it('show appropriate actions for measurement cells', function () {
  246. renderComponent(view, handleCellAction, 5);
  247. openMenu();
  248. expect(
  249. screen.queryByRole('button', {name: 'Add to filter'})
  250. ).not.toBeInTheDocument();
  251. expect(
  252. screen.queryByRole('button', {name: 'Exclude from filter'})
  253. ).not.toBeInTheDocument();
  254. expect(
  255. screen.getByRole('button', {name: 'Show values greater than'})
  256. ).toBeInTheDocument();
  257. expect(
  258. screen.getByRole('button', {name: 'Show values less than'})
  259. ).toBeInTheDocument();
  260. });
  261. it('show appropriate actions for empty measurement cells', function () {
  262. renderComponent(view, handleCellAction, 5, {
  263. ...defaultData,
  264. 'measurements.fcp': null,
  265. });
  266. openMenu();
  267. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  268. expect(
  269. screen.getByRole('button', {name: 'Exclude from filter'})
  270. ).toBeInTheDocument();
  271. expect(
  272. screen.queryByRole('button', {name: 'Show values greater than'})
  273. ).not.toBeInTheDocument();
  274. expect(
  275. screen.queryByRole('button', {name: 'Show values less than'})
  276. ).not.toBeInTheDocument();
  277. });
  278. it('show appropriate actions for numeric function cells', function () {
  279. renderComponent(view, handleCellAction, 6);
  280. openMenu();
  281. expect(
  282. screen.getByRole('button', {name: 'Show values greater than'})
  283. ).toBeInTheDocument();
  284. expect(
  285. screen.getByRole('button', {name: 'Show values less than'})
  286. ).toBeInTheDocument();
  287. });
  288. it('show appropriate actions for empty numeric function cells', function () {
  289. renderComponent(view, handleCellAction, 6, {
  290. ...defaultData,
  291. 'percentile(measurements.fcp, 0.5)': null,
  292. });
  293. hoverContainer();
  294. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  295. });
  296. });
  297. });
  298. describe('updateQuery()', function () {
  299. const columnA = {
  300. key: 'a',
  301. name: 'a',
  302. type: 'number',
  303. isSortable: false,
  304. column: {
  305. kind: 'field',
  306. field: 'a',
  307. },
  308. width: -1,
  309. };
  310. const columnB = {
  311. key: 'b',
  312. name: 'b',
  313. type: 'number',
  314. isSortable: false,
  315. column: {
  316. kind: 'field',
  317. field: 'b',
  318. },
  319. width: -1,
  320. };
  321. it('modifies the query with has/!has', function () {
  322. let results = new MutableSearch([]);
  323. updateQuery(results, Actions.ADD, columnA, null);
  324. expect(results.formatString()).toEqual('!has:a');
  325. updateQuery(results, Actions.EXCLUDE, columnA, null);
  326. expect(results.formatString()).toEqual('has:a');
  327. updateQuery(results, Actions.ADD, columnA, null);
  328. expect(results.formatString()).toEqual('!has:a');
  329. results = new MutableSearch([]);
  330. updateQuery(results, Actions.ADD, columnA, [null]);
  331. expect(results.formatString()).toEqual('!has:a');
  332. });
  333. it('modifies the query with additions', function () {
  334. const results = new MutableSearch([]);
  335. updateQuery(results, Actions.ADD, columnA, '1');
  336. expect(results.formatString()).toEqual('a:1');
  337. updateQuery(results, Actions.ADD, columnB, '1');
  338. expect(results.formatString()).toEqual('a:1 b:1');
  339. updateQuery(results, Actions.ADD, columnA, '2');
  340. expect(results.formatString()).toEqual('b:1 a:2');
  341. updateQuery(results, Actions.ADD, columnA, ['1', '2', '3']);
  342. expect(results.formatString()).toEqual('b:1 a:2 a:1 a:3');
  343. });
  344. it('modifies the query with exclusions', function () {
  345. const results = new MutableSearch([]);
  346. updateQuery(results, Actions.EXCLUDE, columnA, '1');
  347. expect(results.formatString()).toEqual('!a:1');
  348. updateQuery(results, Actions.EXCLUDE, columnB, '1');
  349. expect(results.formatString()).toEqual('!a:1 !b:1');
  350. updateQuery(results, Actions.EXCLUDE, columnA, '2');
  351. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2');
  352. updateQuery(results, Actions.EXCLUDE, columnA, ['1', '2', '3']);
  353. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2 !a:3');
  354. });
  355. it('modifies the query with a mix of additions and exclusions', function () {
  356. const results = new MutableSearch([]);
  357. updateQuery(results, Actions.ADD, columnA, '1');
  358. expect(results.formatString()).toEqual('a:1');
  359. updateQuery(results, Actions.ADD, columnB, '2');
  360. expect(results.formatString()).toEqual('a:1 b:2');
  361. updateQuery(results, Actions.EXCLUDE, columnA, '3');
  362. expect(results.formatString()).toEqual('b:2 !a:3');
  363. updateQuery(results, Actions.EXCLUDE, columnB, '4');
  364. expect(results.formatString()).toEqual('!a:3 !b:4');
  365. results.addFilterValues('!a', ['*dontescapeme*'], false);
  366. expect(results.formatString()).toEqual('!a:3 !b:4 !a:*dontescapeme*');
  367. updateQuery(results, Actions.EXCLUDE, columnA, '*escapeme*');
  368. expect(results.formatString()).toEqual(
  369. '!b:4 !a:3 !a:*dontescapeme* !a:"\\*escapeme\\*"'
  370. );
  371. updateQuery(results, Actions.ADD, columnA, '5');
  372. expect(results.formatString()).toEqual('!b:4 a:5');
  373. updateQuery(results, Actions.ADD, columnB, '6');
  374. expect(results.formatString()).toEqual('a:5 b:6');
  375. });
  376. it('modifies the query with greater/less than', function () {
  377. const results = new MutableSearch([]);
  378. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 1);
  379. expect(results.formatString()).toEqual('a:>1');
  380. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 2);
  381. expect(results.formatString()).toEqual('a:>2');
  382. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 3);
  383. expect(results.formatString()).toEqual('a:<3');
  384. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 4);
  385. expect(results.formatString()).toEqual('a:<4');
  386. });
  387. it('modifies the query with greater/less than on duration fields', function () {
  388. const columnADuration = {...columnA, type: 'duration'};
  389. const results = new MutableSearch([]);
  390. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 1);
  391. expect(results.formatString()).toEqual('a:>1.00ms');
  392. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 2);
  393. expect(results.formatString()).toEqual('a:>2.00ms');
  394. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 3);
  395. expect(results.formatString()).toEqual('a:<3.00ms');
  396. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 4.1234);
  397. expect(results.formatString()).toEqual('a:<4.12ms');
  398. });
  399. it('does not error for special actions', function () {
  400. const results = new MutableSearch([]);
  401. updateQuery(results, Actions.TRANSACTION, columnA, '');
  402. updateQuery(results, Actions.RELEASE, columnA, '');
  403. updateQuery(results, Actions.DRILLDOWN, columnA, '');
  404. });
  405. it('errors for unknown actions', function () {
  406. const results = new MutableSearch([]);
  407. expect(() => updateQuery(results, 'unknown', columnA, '')).toThrow();
  408. });
  409. });