cellAction.spec.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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/discover/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. async function hoverContainer() {
  67. await userEvent.hover(screen.getByText('some content'));
  68. }
  69. async function unhoverContainer() {
  70. await userEvent.unhover(screen.getByText('some content'));
  71. }
  72. async function openMenu() {
  73. await hoverContainer();
  74. await 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', async function () {
  82. renderComponent(view);
  83. await hoverContainer();
  84. expect(screen.getByRole('button')).toBeInTheDocument();
  85. await unhoverContainer();
  86. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  87. });
  88. });
  89. describe('opening the menu', function () {
  90. it('toggles the menu on click', async function () {
  91. renderComponent(view);
  92. await 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', async function () {
  102. renderComponent(view, handleCellAction);
  103. await openMenu();
  104. await userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  105. expect(handleCellAction).toHaveBeenCalledWith('add', 'best-transaction');
  106. });
  107. it('exclude button adds condition', async function () {
  108. renderComponent(view, handleCellAction);
  109. await openMenu();
  110. await userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  111. expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
  112. });
  113. it('exclude button appends exclusions', async function () {
  114. const excludeView = EventView.fromLocation({
  115. query: {...location.query, query: '!transaction:nope'},
  116. });
  117. renderComponent(excludeView, handleCellAction);
  118. await openMenu();
  119. await 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', async function () {
  123. renderComponent(view, handleCellAction);
  124. await openMenu();
  125. await 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', async function () {
  129. renderComponent(view, handleCellAction, 3);
  130. await openMenu();
  131. await 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', async function () {
  138. renderComponent(view, handleCellAction, 2);
  139. await openMenu();
  140. await userEvent.click(
  141. screen.getByRole('button', {name: 'Show values greater than'})
  142. );
  143. expect(handleCellAction).toHaveBeenCalledWith(
  144. 'show_greater_than',
  145. '2020-06-09T01:46:25+00:00'
  146. );
  147. });
  148. it('less than button adds condition', async function () {
  149. renderComponent(view, handleCellAction, 2);
  150. await openMenu();
  151. await userEvent.click(screen.getByRole('button', {name: 'Show values less than'}));
  152. expect(handleCellAction).toHaveBeenCalledWith(
  153. 'show_less_than',
  154. '2020-06-09T01:46:25+00:00'
  155. );
  156. });
  157. it('error.handled with null adds condition', async function () {
  158. renderComponent(view, handleCellAction, 7, defaultData);
  159. await openMenu();
  160. await userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  161. expect(handleCellAction).toHaveBeenCalledWith('add', 1);
  162. });
  163. it('error.type with array values adds condition', async function () {
  164. renderComponent(view, handleCellAction, 8, defaultData);
  165. await openMenu();
  166. await userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  167. expect(handleCellAction).toHaveBeenCalledWith('add', [
  168. 'ServerException',
  169. 'ClickhouseError',
  170. 'QueryException',
  171. 'QueryException',
  172. ]);
  173. });
  174. it('error.handled with 0 adds condition', async function () {
  175. renderComponent(view, handleCellAction, 7, {
  176. ...defaultData,
  177. 'error.handled': [0],
  178. });
  179. await openMenu();
  180. await userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  181. expect(handleCellAction).toHaveBeenCalledWith('add', [0]);
  182. });
  183. it('show appropriate actions for string cells', async function () {
  184. renderComponent(view, handleCellAction, 0);
  185. await openMenu();
  186. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  187. expect(
  188. screen.getByRole('button', {name: 'Exclude from filter'})
  189. ).toBeInTheDocument();
  190. expect(
  191. screen.queryByRole('button', {name: 'Show values greater than'})
  192. ).not.toBeInTheDocument();
  193. expect(
  194. screen.queryByRole('button', {name: 'Show values less than'})
  195. ).not.toBeInTheDocument();
  196. });
  197. it('show appropriate actions for string cells with null values', async function () {
  198. renderComponent(view, handleCellAction, 4);
  199. await openMenu();
  200. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  201. expect(
  202. screen.getByRole('button', {name: 'Exclude from filter'})
  203. ).toBeInTheDocument();
  204. });
  205. it('show appropriate actions for number cells', async function () {
  206. renderComponent(view, handleCellAction, 1);
  207. await openMenu();
  208. expect(
  209. screen.queryByRole('button', {name: 'Add to filter'})
  210. ).not.toBeInTheDocument();
  211. expect(
  212. screen.queryByRole('button', {name: 'Exclude from filter'})
  213. ).not.toBeInTheDocument();
  214. expect(
  215. screen.getByRole('button', {name: 'Show values greater than'})
  216. ).toBeInTheDocument();
  217. expect(
  218. screen.getByRole('button', {name: 'Show values less than'})
  219. ).toBeInTheDocument();
  220. });
  221. it('show appropriate actions for date cells', async function () {
  222. renderComponent(view, handleCellAction, 2);
  223. await openMenu();
  224. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  225. expect(
  226. screen.queryByRole('button', {name: 'Exclude from filter'})
  227. ).not.toBeInTheDocument();
  228. expect(
  229. screen.getByRole('button', {name: 'Show values greater than'})
  230. ).toBeInTheDocument();
  231. expect(
  232. screen.getByRole('button', {name: 'Show values less than'})
  233. ).toBeInTheDocument();
  234. });
  235. it('show appropriate actions for release cells', async function () {
  236. renderComponent(view, handleCellAction, 3);
  237. await openMenu();
  238. expect(screen.getByRole('button', {name: 'Go to release'})).toBeInTheDocument();
  239. });
  240. it('show appropriate actions for empty release cells', async function () {
  241. renderComponent(view, handleCellAction, 3, {...defaultData, release: null});
  242. await openMenu();
  243. expect(
  244. screen.queryByRole('button', {name: 'Go to release'})
  245. ).not.toBeInTheDocument();
  246. });
  247. it('show appropriate actions for measurement cells', async function () {
  248. renderComponent(view, handleCellAction, 5);
  249. await openMenu();
  250. expect(
  251. screen.queryByRole('button', {name: 'Add to filter'})
  252. ).not.toBeInTheDocument();
  253. expect(
  254. screen.queryByRole('button', {name: 'Exclude from filter'})
  255. ).not.toBeInTheDocument();
  256. expect(
  257. screen.getByRole('button', {name: 'Show values greater than'})
  258. ).toBeInTheDocument();
  259. expect(
  260. screen.getByRole('button', {name: 'Show values less than'})
  261. ).toBeInTheDocument();
  262. });
  263. it('show appropriate actions for empty measurement cells', async function () {
  264. renderComponent(view, handleCellAction, 5, {
  265. ...defaultData,
  266. 'measurements.fcp': null,
  267. });
  268. await openMenu();
  269. expect(screen.getByRole('button', {name: 'Add to filter'})).toBeInTheDocument();
  270. expect(
  271. screen.getByRole('button', {name: 'Exclude from filter'})
  272. ).toBeInTheDocument();
  273. expect(
  274. screen.queryByRole('button', {name: 'Show values greater than'})
  275. ).not.toBeInTheDocument();
  276. expect(
  277. screen.queryByRole('button', {name: 'Show values less than'})
  278. ).not.toBeInTheDocument();
  279. });
  280. it('show appropriate actions for numeric function cells', async function () {
  281. renderComponent(view, handleCellAction, 6);
  282. await openMenu();
  283. expect(
  284. screen.getByRole('button', {name: 'Show values greater than'})
  285. ).toBeInTheDocument();
  286. expect(
  287. screen.getByRole('button', {name: 'Show values less than'})
  288. ).toBeInTheDocument();
  289. });
  290. it('show appropriate actions for empty numeric function cells', async function () {
  291. renderComponent(view, handleCellAction, 6, {
  292. ...defaultData,
  293. 'percentile(measurements.fcp, 0.5)': null,
  294. });
  295. await hoverContainer();
  296. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  297. });
  298. });
  299. });
  300. describe('updateQuery()', function () {
  301. const columnA = {
  302. key: 'a',
  303. name: 'a',
  304. type: 'number',
  305. isSortable: false,
  306. column: {
  307. kind: 'field',
  308. field: 'a',
  309. },
  310. width: -1,
  311. };
  312. const columnB = {
  313. key: 'b',
  314. name: 'b',
  315. type: 'number',
  316. isSortable: false,
  317. column: {
  318. kind: 'field',
  319. field: 'b',
  320. },
  321. width: -1,
  322. };
  323. it('modifies the query with has/!has', function () {
  324. let results = new MutableSearch([]);
  325. updateQuery(results, Actions.ADD, columnA, null);
  326. expect(results.formatString()).toEqual('!has:a');
  327. updateQuery(results, Actions.EXCLUDE, columnA, null);
  328. expect(results.formatString()).toEqual('has:a');
  329. updateQuery(results, Actions.ADD, columnA, null);
  330. expect(results.formatString()).toEqual('!has:a');
  331. results = new MutableSearch([]);
  332. updateQuery(results, Actions.ADD, columnA, [null]);
  333. expect(results.formatString()).toEqual('!has:a');
  334. });
  335. it('modifies the query with additions', function () {
  336. const results = new MutableSearch([]);
  337. updateQuery(results, Actions.ADD, columnA, '1');
  338. expect(results.formatString()).toEqual('a:1');
  339. updateQuery(results, Actions.ADD, columnB, '1');
  340. expect(results.formatString()).toEqual('a:1 b:1');
  341. updateQuery(results, Actions.ADD, columnA, '2');
  342. expect(results.formatString()).toEqual('b:1 a:2');
  343. updateQuery(results, Actions.ADD, columnA, ['1', '2', '3']);
  344. expect(results.formatString()).toEqual('b:1 a:2 a:1 a:3');
  345. });
  346. it('modifies the query with exclusions', function () {
  347. const results = new MutableSearch([]);
  348. updateQuery(results, Actions.EXCLUDE, columnA, '1');
  349. expect(results.formatString()).toEqual('!a:1');
  350. updateQuery(results, Actions.EXCLUDE, columnB, '1');
  351. expect(results.formatString()).toEqual('!a:1 !b:1');
  352. updateQuery(results, Actions.EXCLUDE, columnA, '2');
  353. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2');
  354. updateQuery(results, Actions.EXCLUDE, columnA, ['1', '2', '3']);
  355. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2 !a:3');
  356. });
  357. it('modifies the query with a mix of additions and exclusions', function () {
  358. const results = new MutableSearch([]);
  359. updateQuery(results, Actions.ADD, columnA, '1');
  360. expect(results.formatString()).toEqual('a:1');
  361. updateQuery(results, Actions.ADD, columnB, '2');
  362. expect(results.formatString()).toEqual('a:1 b:2');
  363. updateQuery(results, Actions.EXCLUDE, columnA, '3');
  364. expect(results.formatString()).toEqual('b:2 !a:3');
  365. updateQuery(results, Actions.EXCLUDE, columnB, '4');
  366. expect(results.formatString()).toEqual('!a:3 !b:4');
  367. results.addFilterValues('!a', ['*dontescapeme*'], false);
  368. expect(results.formatString()).toEqual('!a:3 !b:4 !a:*dontescapeme*');
  369. updateQuery(results, Actions.EXCLUDE, columnA, '*escapeme*');
  370. expect(results.formatString()).toEqual(
  371. '!b:4 !a:3 !a:*dontescapeme* !a:"\\*escapeme\\*"'
  372. );
  373. updateQuery(results, Actions.ADD, columnA, '5');
  374. expect(results.formatString()).toEqual('!b:4 a:5');
  375. updateQuery(results, Actions.ADD, columnB, '6');
  376. expect(results.formatString()).toEqual('a:5 b:6');
  377. });
  378. it('modifies the query with greater/less than', function () {
  379. const results = new MutableSearch([]);
  380. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 1);
  381. expect(results.formatString()).toEqual('a:>1');
  382. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 2);
  383. expect(results.formatString()).toEqual('a:>2');
  384. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 3);
  385. expect(results.formatString()).toEqual('a:<3');
  386. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 4);
  387. expect(results.formatString()).toEqual('a:<4');
  388. });
  389. it('modifies the query with greater/less than on duration fields', function () {
  390. const columnADuration = {...columnA, type: 'duration'};
  391. const results = new MutableSearch([]);
  392. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 1);
  393. expect(results.formatString()).toEqual('a:>1.00ms');
  394. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 2);
  395. expect(results.formatString()).toEqual('a:>2.00ms');
  396. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 3);
  397. expect(results.formatString()).toEqual('a:<3.00ms');
  398. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 4.1234);
  399. expect(results.formatString()).toEqual('a:<4.12ms');
  400. });
  401. it('does not error for special actions', function () {
  402. const results = new MutableSearch([]);
  403. updateQuery(results, Actions.TRANSACTION, columnA, '');
  404. updateQuery(results, Actions.RELEASE, columnA, '');
  405. updateQuery(results, Actions.DRILLDOWN, columnA, '');
  406. });
  407. it('errors for unknown actions', function () {
  408. const results = new MutableSearch([]);
  409. expect(() => updateQuery(results, 'unknown', columnA, '')).toThrow();
  410. });
  411. });