cellAction.spec.jsx 15 KB

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