tableView.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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('renders transaction summary link', function () {
  242. rows.data[0].project = 'project-slug';
  243. renderComponent(initialData, rows, eventView);
  244. const firstRow = screen.getAllByRole('row')[1];
  245. const link = within(firstRow).getByTestId('tableView-transaction-link');
  246. expect(link).toHaveAttribute(
  247. 'href',
  248. expect.stringMatching(
  249. RegExp(
  250. '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
  251. )
  252. )
  253. );
  254. });
  255. it('handles go to release', async function () {
  256. renderComponent(initialData, rows, eventView);
  257. await openContextMenu(5);
  258. await userEvent.click(screen.getByRole('button', {name: 'Go to release'}));
  259. expect(browserHistory.push).toHaveBeenCalledWith({
  260. pathname: '/organizations/org-slug/releases/v1.0.2/',
  261. query: expect.objectContaining({
  262. environment: eventView.environment,
  263. }),
  264. });
  265. });
  266. it('has title on integer value greater than 999', function () {
  267. rows.data[0]['count()'] = 1000;
  268. renderComponent(initialData, rows, eventView);
  269. const firstRow = screen.getAllByRole('row')[1];
  270. const emptyValueCell = within(firstRow).getAllByRole('cell')[3];
  271. expect(within(emptyValueCell).getByText('1k')).toHaveAttribute('title', '1,000');
  272. });
  273. it('renders size columns correctly', function () {
  274. const orgWithFeature = TestStubs.Organization({
  275. projects: [TestStubs.Project()],
  276. });
  277. render(
  278. <TableView
  279. organization={orgWithFeature}
  280. location={location}
  281. eventView={EventView.fromLocation({
  282. ...location,
  283. query: {
  284. ...location.query,
  285. field: [
  286. 'title',
  287. 'p99(measurements.custom.kibibyte)',
  288. 'p99(measurements.custom.kilobyte)',
  289. ],
  290. },
  291. })}
  292. isLoading={false}
  293. projects={initialData.organization.projects}
  294. tableData={{
  295. data: [
  296. {
  297. title: '/random/transaction/name',
  298. 'p99(measurements.custom.kibibyte)': 222.3,
  299. 'p99(measurements.custom.kilobyte)': 444.3,
  300. },
  301. ],
  302. meta: {
  303. title: 'string',
  304. 'p99(measurements.custom.kibibyte)': 'size',
  305. 'p99(measurements.custom.kilobyte)': 'size',
  306. units: {
  307. title: null,
  308. 'p99(measurements.custom.kibibyte)': 'kibibyte',
  309. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  310. },
  311. },
  312. }}
  313. onChangeShowTags={onChangeShowTags}
  314. />
  315. );
  316. expect(screen.getByText('222.3 KiB')).toBeInTheDocument();
  317. expect(screen.getByText('444.3 KB')).toBeInTheDocument();
  318. });
  319. it('shows events with value less than selected custom performance metric', async function () {
  320. const orgWithFeature = TestStubs.Organization({
  321. projects: [TestStubs.Project()],
  322. });
  323. render(
  324. <TableView
  325. organization={orgWithFeature}
  326. location={location}
  327. eventView={EventView.fromLocation({
  328. ...location,
  329. query: {
  330. ...location.query,
  331. field: ['title', 'p99(measurements.custom.kilobyte)'],
  332. },
  333. })}
  334. isLoading={false}
  335. projects={initialData.organization.projects}
  336. tableData={{
  337. data: [
  338. {
  339. title: '/random/transaction/name',
  340. 'p99(measurements.custom.kilobyte)': 444.3,
  341. },
  342. ],
  343. meta: {
  344. title: 'string',
  345. 'p99(measurements.custom.kilobyte)': 'size',
  346. units: {
  347. title: null,
  348. 'p99(measurements.custom.kilobyte)': 'kilobyte',
  349. },
  350. },
  351. }}
  352. onChangeShowTags={onChangeShowTags}
  353. />
  354. );
  355. await userEvent.hover(screen.getByText('444.3 KB'));
  356. const buttons = screen.getAllByRole('button');
  357. await userEvent.click(buttons[buttons.length - 1]);
  358. await userEvent.click(screen.getByText('Show values less than'));
  359. expect(browserHistory.push).toHaveBeenCalledWith({
  360. pathname: location.pathname,
  361. query: expect.objectContaining({
  362. query: 'p99(measurements.custom.kilobyte):<444300',
  363. }),
  364. });
  365. });
  366. });