tableView.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import {browserHistory} from 'react-router';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  4. import ProjectsStore from 'sentry/stores/projectsStore';
  5. import TagStore from 'sentry/stores/tagStore';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import TableView from 'sentry/views/eventsV2/table/tableView';
  8. describe('TableView > CellActions', function () {
  9. let initialData, rows, onChangeShowTags;
  10. const location = {
  11. pathname: '/organizations/org-slug/discover/results/',
  12. query: {
  13. id: '42',
  14. name: 'best query',
  15. field: [
  16. 'title',
  17. 'transaction',
  18. 'count()',
  19. 'timestamp',
  20. 'release',
  21. 'equation|count() + 100',
  22. ],
  23. sort: ['title'],
  24. query: '',
  25. project: [123],
  26. statsPeriod: '14d',
  27. environment: ['staging'],
  28. yAxis: 'p95',
  29. },
  30. };
  31. const eventView = EventView.fromLocation(location);
  32. function renderComponent(context, tableData, view) {
  33. return render(
  34. <TableView
  35. organization={context.organization}
  36. location={location}
  37. eventView={view}
  38. isLoading={false}
  39. projects={context.organization.projects}
  40. tableData={tableData}
  41. onChangeShowTags={onChangeShowTags}
  42. />,
  43. {context: context.routerContext}
  44. );
  45. }
  46. function openContextMenu(cellIndex) {
  47. const firstRow = screen.getAllByRole('row')[1];
  48. const emptyValueCell = within(firstRow).getAllByRole('cell')[cellIndex];
  49. userEvent.hover(within(emptyValueCell).getByTestId('cell-action-container'));
  50. userEvent.click(within(emptyValueCell).getByRole('button'));
  51. }
  52. beforeEach(function () {
  53. browserHistory.push.mockReset();
  54. browserHistory.replace.mockReset();
  55. const organization = TestStubs.Organization({
  56. features: ['discover-basic', 'discover-frontend-use-events-endpoint'],
  57. projects: [TestStubs.Project()],
  58. });
  59. initialData = initializeOrg({
  60. organization,
  61. router: {location},
  62. });
  63. act(() => {
  64. ProjectsStore.loadInitialData(initialData.organization.projects);
  65. TagStore.reset();
  66. TagStore.loadTagsSuccess([
  67. {name: 'size', key: 'size', count: 1},
  68. {name: 'shape', key: 'shape', count: 1},
  69. {name: 'direction', key: 'direction', count: 1},
  70. ]);
  71. });
  72. onChangeShowTags = jest.fn();
  73. rows = {
  74. meta: {
  75. title: 'string',
  76. transaction: 'string',
  77. 'count()': 'integer',
  78. timestamp: 'date',
  79. release: 'string',
  80. 'equation[0]': 'integer',
  81. },
  82. data: [
  83. {
  84. title: 'some title',
  85. transaction: '/organizations/',
  86. 'count()': 9,
  87. timestamp: '2019-05-23T22:12:48+00:00',
  88. release: 'v1.0.2',
  89. 'equation[0]': 109,
  90. },
  91. ],
  92. };
  93. });
  94. afterEach(() => {
  95. ProjectsStore.reset();
  96. });
  97. it('updates sort order on equation fields', function () {
  98. const view = eventView.clone();
  99. renderComponent(initialData, rows, view);
  100. const equationCell = screen.getByRole('columnheader', {name: 'count() + 100'});
  101. const sortLink = within(equationCell).getByRole('link');
  102. expect(sortLink).toHaveAttribute(
  103. 'href',
  104. '/organizations/org-slug/discover/results/?environment=staging&field=title&field=transaction&field=count%28%29&field=timestamp&field=release&field=equation%7Ccount%28%29%20%2B%20100&id=42&name=best%20query&project=123&query=&sort=-equation%7Ccount%28%29%20%2B%20100&statsPeriod=14d&yAxis=p95'
  105. );
  106. });
  107. it('updates sort order on non-equation fields', function () {
  108. const view = eventView.clone();
  109. renderComponent(initialData, rows, view);
  110. const transactionCell = screen.getByRole('columnheader', {name: 'transaction'});
  111. const sortLink = within(transactionCell).getByRole('link');
  112. expect(sortLink).toHaveAttribute(
  113. 'href',
  114. '/organizations/org-slug/discover/results/?environment=staging&field=title&field=transaction&field=count%28%29&field=timestamp&field=release&field=equation%7Ccount%28%29%20%2B%20100&id=42&name=best%20query&project=123&query=&sort=-transaction&statsPeriod=14d&yAxis=p95'
  115. );
  116. });
  117. it('handles add cell action on null value', function () {
  118. rows.data[0].title = null;
  119. renderComponent(initialData, rows, eventView);
  120. openContextMenu(1);
  121. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  122. expect(browserHistory.push).toHaveBeenCalledWith({
  123. pathname: location.pathname,
  124. query: expect.objectContaining({
  125. query: '!has:title',
  126. }),
  127. });
  128. });
  129. it('handles add cell action on null value replace has condition', function () {
  130. rows.data[0].title = null;
  131. const view = eventView.clone();
  132. view.query = 'tag:value has:title';
  133. renderComponent(initialData, rows, view);
  134. openContextMenu(1);
  135. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  136. expect(browserHistory.push).toHaveBeenCalledWith({
  137. pathname: location.pathname,
  138. query: expect.objectContaining({
  139. query: 'tag:value !has:title',
  140. }),
  141. });
  142. });
  143. it('handles add cell action on string value replace negation', function () {
  144. const view = eventView.clone();
  145. view.query = 'tag:value !title:nope';
  146. renderComponent(initialData, rows, view);
  147. openContextMenu(1);
  148. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  149. expect(browserHistory.push).toHaveBeenCalledWith({
  150. pathname: location.pathname,
  151. query: expect.objectContaining({
  152. query: 'tag:value title:"some title"',
  153. }),
  154. });
  155. });
  156. it('handles add cell action with multiple y axis', function () {
  157. location.query.yAxis = ['count()', 'failure_count()'];
  158. renderComponent(initialData, rows, eventView);
  159. openContextMenu(1);
  160. userEvent.click(screen.getByRole('button', {name: 'Add to filter'}));
  161. expect(browserHistory.push).toHaveBeenCalledWith({
  162. pathname: location.pathname,
  163. query: expect.objectContaining({
  164. query: 'title:"some title"',
  165. yAxis: ['count()', 'failure_count()'],
  166. }),
  167. });
  168. });
  169. it('handles exclude cell action on string value', function () {
  170. renderComponent(initialData, rows, eventView);
  171. openContextMenu(1);
  172. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  173. expect(browserHistory.push).toHaveBeenCalledWith({
  174. pathname: location.pathname,
  175. query: expect.objectContaining({
  176. query: '!title:"some title"',
  177. }),
  178. });
  179. });
  180. it('handles exclude cell action on string value replace inclusion', function () {
  181. const view = eventView.clone();
  182. view.query = 'tag:value title:nope';
  183. renderComponent(initialData, rows, view);
  184. openContextMenu(1);
  185. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  186. expect(browserHistory.push).toHaveBeenCalledWith({
  187. pathname: location.pathname,
  188. query: expect.objectContaining({
  189. query: 'tag:value !title:"some title"',
  190. }),
  191. });
  192. });
  193. it('handles exclude cell action on null value', function () {
  194. rows.data[0].title = null;
  195. renderComponent(initialData, rows, eventView);
  196. openContextMenu(1);
  197. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  198. expect(browserHistory.push).toHaveBeenCalledWith({
  199. pathname: location.pathname,
  200. query: expect.objectContaining({
  201. query: 'has:title',
  202. }),
  203. });
  204. });
  205. it('handles exclude cell action on null value replace condition', function () {
  206. const view = eventView.clone();
  207. view.query = 'tag:value !has:title';
  208. rows.data[0].title = null;
  209. renderComponent(initialData, rows, view);
  210. openContextMenu(1);
  211. userEvent.click(screen.getByRole('button', {name: 'Exclude from filter'}));
  212. expect(browserHistory.push).toHaveBeenCalledWith({
  213. pathname: location.pathname,
  214. query: expect.objectContaining({
  215. query: 'tag:value has:title',
  216. }),
  217. });
  218. });
  219. it('handles greater than cell action on number value', function () {
  220. renderComponent(initialData, rows, eventView);
  221. openContextMenu(3);
  222. userEvent.click(screen.getByRole('button', {name: 'Show values greater than'}));
  223. expect(browserHistory.push).toHaveBeenCalledWith({
  224. pathname: location.pathname,
  225. query: expect.objectContaining({
  226. query: 'count():>9',
  227. }),
  228. });
  229. });
  230. it('handles less than cell action on number value', function () {
  231. renderComponent(initialData, rows, eventView);
  232. openContextMenu(3);
  233. userEvent.click(screen.getByRole('button', {name: 'Show values less than'}));
  234. expect(browserHistory.push).toHaveBeenCalledWith({
  235. pathname: location.pathname,
  236. query: expect.objectContaining({
  237. query: 'count():<9',
  238. }),
  239. });
  240. });
  241. it('handles go to transaction without project column selected', function () {
  242. rows.data[0]['project.name'] = 'project-slug';
  243. renderComponent(initialData, rows, eventView);
  244. openContextMenu(2);
  245. userEvent.click(screen.getByRole('button', {name: 'Go to summary'}));
  246. expect(browserHistory.push).toHaveBeenCalledWith({
  247. pathname: '/organizations/org-slug/performance/summary/',
  248. query: expect.objectContaining({
  249. transaction: '/organizations/',
  250. project: ['2'],
  251. }),
  252. });
  253. });
  254. it('handles go to transaction with project column selected', function () {
  255. rows.data[0].project = 'project-slug';
  256. renderComponent(initialData, rows, eventView);
  257. openContextMenu(2);
  258. userEvent.click(screen.getByRole('button', {name: 'Go to summary'}));
  259. expect(browserHistory.push).toHaveBeenCalledWith({
  260. pathname: '/organizations/org-slug/performance/summary/',
  261. query: expect.objectContaining({
  262. transaction: '/organizations/',
  263. project: ['2'],
  264. }),
  265. });
  266. });
  267. it('handles go to release', function () {
  268. renderComponent(initialData, rows, eventView);
  269. openContextMenu(5);
  270. userEvent.click(screen.getByRole('button', {name: 'Go to release'}));
  271. expect(browserHistory.push).toHaveBeenCalledWith({
  272. pathname: '/organizations/org-slug/releases/v1.0.2/',
  273. query: expect.objectContaining({
  274. environment: eventView.environment,
  275. }),
  276. });
  277. });
  278. it('has title on integer value greater than 999', function () {
  279. rows.data[0]['count()'] = 1000;
  280. renderComponent(initialData, rows, eventView);
  281. const firstRow = screen.getAllByRole('row')[1];
  282. const emptyValueCell = within(firstRow).getAllByRole('cell')[3];
  283. expect(within(emptyValueCell).getByText('1k')).toHaveAttribute('title', '1,000');
  284. });
  285. it('renders size columns correctly', function () {
  286. const orgWithFeature = TestStubs.Organization({
  287. features: ['discover-frontend-use-events-endpoint'],
  288. projects: [TestStubs.Project()],
  289. });
  290. render(
  291. <TableView
  292. organization={orgWithFeature}
  293. location={location}
  294. eventView={EventView.fromLocation({
  295. ...location,
  296. query: {
  297. ...location.query,
  298. field: [
  299. 'title',
  300. 'p99(measurements.custom.kibibyte)',
  301. 'p99(measurements.custom.kilobyte)',
  302. ],
  303. },
  304. })}
  305. isLoading={false}
  306. projects={initialData.organization.projects}
  307. tableData={{
  308. data: [
  309. {
  310. title: '/random/transaction/name',
  311. 'p99(measurements.custom.kibibyte)': 222.3,
  312. 'p99(measurements.custom.kilobyte)': 444.3,
  313. },
  314. ],
  315. meta: {
  316. title: 'string',
  317. 'p99(measurements.custom.kibibyte)': 'size',
  318. 'p99(measurements.custom.kilobyte)': 'size',
  319. units: {
  320. title: null,
  321. 'p99(measurements.custom.kibibyte)': 'kibibyte',
  322. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  323. },
  324. },
  325. }}
  326. onChangeShowTags={onChangeShowTags}
  327. />
  328. );
  329. expect(screen.getByText('222.3 KiB')).toBeInTheDocument();
  330. expect(screen.getByText('444.3 KB')).toBeInTheDocument();
  331. });
  332. it('shows events with value less than selected custom performance metric', function () {
  333. const orgWithFeature = TestStubs.Organization({
  334. features: ['discover-frontend-use-events-endpoint'],
  335. projects: [TestStubs.Project()],
  336. });
  337. render(
  338. <TableView
  339. organization={orgWithFeature}
  340. location={location}
  341. eventView={EventView.fromLocation({
  342. ...location,
  343. query: {
  344. ...location.query,
  345. field: ['title', 'p99(measurements.custom.kilobyte)'],
  346. },
  347. })}
  348. isLoading={false}
  349. projects={initialData.organization.projects}
  350. tableData={{
  351. data: [
  352. {
  353. title: '/random/transaction/name',
  354. 'p99(measurements.custom.kilobyte)': 444.3,
  355. },
  356. ],
  357. meta: {
  358. title: 'string',
  359. 'p99(measurements.custom.kilobyte)': 'size',
  360. units: {
  361. title: null,
  362. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  363. },
  364. },
  365. }}
  366. onChangeShowTags={onChangeShowTags}
  367. />
  368. );
  369. userEvent.hover(screen.getByText('444.3 KB'));
  370. const buttons = screen.getAllByRole('button');
  371. userEvent.click(buttons[buttons.length - 1]);
  372. userEvent.click(screen.getByText('Show values less than'));
  373. expect(browserHistory.push).toHaveBeenCalledWith({
  374. pathname: location.pathname,
  375. query: expect.objectContaining({
  376. query: 'p99(measurements.custom.kilobyte):<444300',
  377. }),
  378. });
  379. });
  380. });