tableView.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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/discover/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. async function openContextMenu(cellIndex) {
  47. const firstRow = screen.getAllByRole('row')[1];
  48. const emptyValueCell = within(firstRow).getAllByRole('cell')[cellIndex];
  49. await userEvent.hover(within(emptyValueCell).getByTestId('cell-action-container'));
  50. await 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'],
  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', async function () {
  118. rows.data[0].title = null;
  119. renderComponent(initialData, rows, eventView);
  120. await openContextMenu(1);
  121. await 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', async 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. await openContextMenu(1);
  135. await 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', async function () {
  144. const view = eventView.clone();
  145. view.query = 'tag:value !title:nope';
  146. renderComponent(initialData, rows, view);
  147. await openContextMenu(1);
  148. await 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', async function () {
  157. location.query.yAxis = ['count()', 'failure_count()'];
  158. renderComponent(initialData, rows, eventView);
  159. await openContextMenu(1);
  160. await 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', async function () {
  170. renderComponent(initialData, rows, eventView);
  171. await openContextMenu(1);
  172. await 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', async function () {
  181. const view = eventView.clone();
  182. view.query = 'tag:value title:nope';
  183. renderComponent(initialData, rows, view);
  184. await openContextMenu(1);
  185. await 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', async function () {
  194. rows.data[0].title = null;
  195. renderComponent(initialData, rows, eventView);
  196. await openContextMenu(1);
  197. await 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', async 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. await openContextMenu(1);
  211. await 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', async function () {
  220. renderComponent(initialData, rows, eventView);
  221. await openContextMenu(3);
  222. await 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', async function () {
  231. renderComponent(initialData, rows, eventView);
  232. await openContextMenu(3);
  233. await 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', async function () {
  242. rows.data[0]['project.name'] = 'project-slug';
  243. renderComponent(initialData, rows, eventView);
  244. await openContextMenu(2);
  245. await 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', async function () {
  255. rows.data[0].project = 'project-slug';
  256. renderComponent(initialData, rows, eventView);
  257. await openContextMenu(2);
  258. await 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('renders transaction summary link', function () {
  268. rows.data[0].project = 'project-slug';
  269. renderComponent(initialData, rows, eventView);
  270. const firstRow = screen.getAllByRole('row')[1];
  271. const link = within(firstRow).getByTestId('tableView-transaction-link');
  272. expect(link).toHaveAttribute(
  273. 'href',
  274. expect.stringMatching(
  275. RegExp(
  276. '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
  277. )
  278. )
  279. );
  280. });
  281. it('handles go to release', async function () {
  282. renderComponent(initialData, rows, eventView);
  283. await openContextMenu(5);
  284. await userEvent.click(screen.getByRole('button', {name: 'Go to release'}));
  285. expect(browserHistory.push).toHaveBeenCalledWith({
  286. pathname: '/organizations/org-slug/releases/v1.0.2/',
  287. query: expect.objectContaining({
  288. environment: eventView.environment,
  289. }),
  290. });
  291. });
  292. it('has title on integer value greater than 999', function () {
  293. rows.data[0]['count()'] = 1000;
  294. renderComponent(initialData, rows, eventView);
  295. const firstRow = screen.getAllByRole('row')[1];
  296. const emptyValueCell = within(firstRow).getAllByRole('cell')[3];
  297. expect(within(emptyValueCell).getByText('1k')).toHaveAttribute('title', '1,000');
  298. });
  299. it('renders size columns correctly', function () {
  300. const orgWithFeature = TestStubs.Organization({
  301. projects: [TestStubs.Project()],
  302. });
  303. render(
  304. <TableView
  305. organization={orgWithFeature}
  306. location={location}
  307. eventView={EventView.fromLocation({
  308. ...location,
  309. query: {
  310. ...location.query,
  311. field: [
  312. 'title',
  313. 'p99(measurements.custom.kibibyte)',
  314. 'p99(measurements.custom.kilobyte)',
  315. ],
  316. },
  317. })}
  318. isLoading={false}
  319. projects={initialData.organization.projects}
  320. tableData={{
  321. data: [
  322. {
  323. title: '/random/transaction/name',
  324. 'p99(measurements.custom.kibibyte)': 222.3,
  325. 'p99(measurements.custom.kilobyte)': 444.3,
  326. },
  327. ],
  328. meta: {
  329. title: 'string',
  330. 'p99(measurements.custom.kibibyte)': 'size',
  331. 'p99(measurements.custom.kilobyte)': 'size',
  332. units: {
  333. title: null,
  334. 'p99(measurements.custom.kibibyte)': 'kibibyte',
  335. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  336. },
  337. },
  338. }}
  339. onChangeShowTags={onChangeShowTags}
  340. />
  341. );
  342. expect(screen.getByText('222.3 KiB')).toBeInTheDocument();
  343. expect(screen.getByText('444.3 KB')).toBeInTheDocument();
  344. });
  345. it('shows events with value less than selected custom performance metric', async function () {
  346. const orgWithFeature = TestStubs.Organization({
  347. projects: [TestStubs.Project()],
  348. });
  349. render(
  350. <TableView
  351. organization={orgWithFeature}
  352. location={location}
  353. eventView={EventView.fromLocation({
  354. ...location,
  355. query: {
  356. ...location.query,
  357. field: ['title', 'p99(measurements.custom.kilobyte)'],
  358. },
  359. })}
  360. isLoading={false}
  361. projects={initialData.organization.projects}
  362. tableData={{
  363. data: [
  364. {
  365. title: '/random/transaction/name',
  366. 'p99(measurements.custom.kilobyte)': 444.3,
  367. },
  368. ],
  369. meta: {
  370. title: 'string',
  371. 'p99(measurements.custom.kilobyte)': 'size',
  372. units: {
  373. title: null,
  374. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  375. },
  376. },
  377. }}
  378. onChangeShowTags={onChangeShowTags}
  379. />
  380. );
  381. await userEvent.hover(screen.getByText('444.3 KB'));
  382. const buttons = screen.getAllByRole('button');
  383. await userEvent.click(buttons[buttons.length - 1]);
  384. await userEvent.click(screen.getByText('Show values less than'));
  385. expect(browserHistory.push).toHaveBeenCalledWith({
  386. pathname: location.pathname,
  387. query: expect.objectContaining({
  388. query: 'p99(measurements.custom.kilobyte):<444300',
  389. }),
  390. });
  391. });
  392. });