columnEditModal.spec.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. import styled from '@emotion/styled';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {
  4. fireEvent,
  5. render,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. within,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {makeCloseButton} from 'sentry/components/globalModal/components';
  12. import TagStore from 'sentry/stores/tagStore';
  13. import type {QueryFieldValue} from 'sentry/utils/discover/fields';
  14. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  15. import ColumnEditModal from 'sentry/views/discover/table/columnEditModal';
  16. const stubEl = styled((p: any) => p.children);
  17. function mountModal(
  18. {
  19. columns,
  20. onApply,
  21. customMeasurements,
  22. dataset,
  23. }: Pick<
  24. React.ComponentProps<typeof ColumnEditModal>,
  25. 'columns' | 'onApply' | 'customMeasurements' | 'dataset'
  26. >,
  27. initialData: ReturnType<typeof initializeOrg>
  28. ) {
  29. return render(
  30. <ColumnEditModal
  31. CloseButton={makeCloseButton(() => {})}
  32. Header={c => <div>{c.children}</div>}
  33. Footer={stubEl()}
  34. Body={stubEl()}
  35. organization={initialData.organization}
  36. columns={columns}
  37. onApply={onApply}
  38. closeModal={jest.fn()}
  39. measurementKeys={null}
  40. customMeasurements={customMeasurements}
  41. dataset={dataset}
  42. />,
  43. {router: initialData.router}
  44. );
  45. }
  46. // Get all queryField components which represent a row in the column editor.
  47. const findAllQueryFields = () => screen.findAllByTestId('queryField');
  48. // Get the nth label (value) within the row of the column editor.
  49. const findAllQueryFieldNthCell = async (nth: number) =>
  50. (await findAllQueryFields())
  51. .map(f => within(f).getAllByTestId('label')[nth])
  52. .filter(Boolean);
  53. const getAllQueryFields = () => screen.getAllByTestId('queryField');
  54. const getAllQueryFieldsNthCell = (nth: number) =>
  55. getAllQueryFields()
  56. .map(f => within(f).getAllByTestId('label')[nth])
  57. .filter(Boolean);
  58. const openMenu = async (row: number, column = 0) => {
  59. const queryFields = await screen.findAllByTestId('queryField');
  60. const queryField = queryFields[row]!;
  61. expect(queryField).toBeInTheDocument();
  62. const labels = within(queryField).queryAllByTestId('label');
  63. if (labels.length > 0) {
  64. await userEvent.click(labels[column]!);
  65. } else {
  66. // For test adding a new column, no existing label.
  67. await userEvent.click(screen.getByText('(Required)'));
  68. }
  69. };
  70. const selectByLabel = async (
  71. label: string,
  72. options: {at: number; control?: boolean; name?: string}
  73. ) => {
  74. await openMenu(options.at);
  75. const menuOptions = screen.getAllByTestId('menu-list-item-label'); // TODO: Can likely switch to menuitem role and match against label
  76. const opt = menuOptions.find(e => e.textContent?.includes(label));
  77. await userEvent.click(opt!);
  78. };
  79. describe('Discover -> ColumnEditModal', function () {
  80. beforeEach(() => {
  81. TagStore.reset();
  82. TagStore.loadTagsSuccess([
  83. {name: 'browser.name', key: 'browser.name'},
  84. {name: 'custom-field', key: 'custom-field'},
  85. {name: 'user', key: 'user'},
  86. ]);
  87. });
  88. const initialData = initializeOrg({
  89. organization: {
  90. features: ['performance-view', 'dashboards-mep'],
  91. },
  92. });
  93. const columns: QueryFieldValue[] = [
  94. {
  95. kind: 'field',
  96. field: 'event.type',
  97. },
  98. {
  99. kind: 'field',
  100. field: 'browser.name',
  101. },
  102. {
  103. kind: 'function',
  104. function: ['count', 'id', '', ''],
  105. },
  106. {
  107. kind: 'function',
  108. function: ['count_unique', 'title', '', ''],
  109. },
  110. {
  111. kind: 'function',
  112. function: ['p95', '', '', ''],
  113. },
  114. {
  115. kind: 'field',
  116. field: 'issue.id',
  117. },
  118. {
  119. kind: 'function',
  120. function: ['count_unique', 'issue.id', '', ''],
  121. },
  122. ];
  123. describe('basic rendering', function () {
  124. it('renders fields and basic controls, async delete and grab buttons', async function () {
  125. mountModal(
  126. {
  127. columns,
  128. onApply: jest.fn(),
  129. customMeasurements: {},
  130. },
  131. initialData
  132. );
  133. // Should have fields equal to the columns.
  134. expect((await findAllQueryFieldNthCell(0)).map(el => el!.textContent)).toEqual([
  135. 'event.type',
  136. 'browser.name',
  137. 'count()',
  138. 'count_unique(…)',
  139. 'p95(…)',
  140. 'issue.id',
  141. 'count_unique(…)', // extra because of the function
  142. ]);
  143. expect(screen.getByRole('button', {name: 'Apply'})).toBeInTheDocument();
  144. expect(screen.getByRole('button', {name: 'Add a Column'})).toBeInTheDocument();
  145. expect(screen.getAllByRole('button', {name: 'Remove column'})).toHaveLength(
  146. columns.length
  147. );
  148. expect(screen.getAllByRole('button', {name: 'Drag to reorder'})).toHaveLength(
  149. columns.length
  150. );
  151. });
  152. });
  153. describe('rendering unknown fields', function () {
  154. it('renders unknown fields in field and field parameter controls', async function () {
  155. mountModal(
  156. {
  157. columns: [
  158. {
  159. kind: 'function',
  160. function: ['count_unique', 'user-defined', undefined, undefined],
  161. },
  162. {kind: 'field', field: 'user-def'},
  163. ],
  164. onApply: jest.fn(),
  165. customMeasurements: {},
  166. },
  167. initialData
  168. );
  169. expect((await findAllQueryFieldNthCell(0)).map(el => el!.textContent)).toEqual([
  170. 'count_unique(…)',
  171. 'user-def',
  172. ]);
  173. expect(getAllQueryFieldsNthCell(1).map(el => el!.textContent)).toEqual([
  174. 'user-defined',
  175. ]);
  176. });
  177. });
  178. describe('rendering tags that overlap fields & functions', function () {
  179. beforeEach(() => {
  180. TagStore.reset();
  181. TagStore.loadTagsSuccess([
  182. {name: 'project', key: 'project'},
  183. {name: 'count', key: 'count'},
  184. ]);
  185. });
  186. it('selects tag expressions that overlap fields', async function () {
  187. mountModal(
  188. {
  189. columns: [
  190. {kind: 'field', field: 'tags[project]'},
  191. {kind: 'field', field: 'tags[count]'},
  192. ],
  193. onApply: jest.fn(),
  194. customMeasurements: {},
  195. },
  196. initialData
  197. );
  198. expect((await findAllQueryFieldNthCell(0)).map(el => el!.textContent)).toEqual([
  199. 'project',
  200. 'count',
  201. ]);
  202. });
  203. it('selects tag expressions that overlap functions', async function () {
  204. mountModal(
  205. {
  206. columns: [
  207. {kind: 'field', field: 'tags[project]'},
  208. {kind: 'field', field: 'tags[count]'},
  209. ],
  210. onApply: jest.fn(),
  211. customMeasurements: {},
  212. },
  213. initialData
  214. );
  215. expect((await findAllQueryFieldNthCell(0)).map(el => el!.textContent)).toEqual([
  216. 'project',
  217. 'count',
  218. ]);
  219. });
  220. });
  221. describe('rendering functions', function () {
  222. it('renders three columns when needed', async function () {
  223. mountModal(
  224. {
  225. columns: [
  226. {kind: 'function', function: ['count', 'id', undefined, undefined]},
  227. {kind: 'function', function: ['count_unique', 'title', undefined, undefined]},
  228. {
  229. kind: 'function',
  230. function: ['percentile', 'transaction.duration', '0.66', undefined],
  231. },
  232. ],
  233. onApply: jest.fn(),
  234. customMeasurements: {},
  235. },
  236. initialData
  237. );
  238. const queryFields = await findAllQueryFields();
  239. const countRow = queryFields[0]!;
  240. expect(
  241. within(countRow)
  242. .getAllByTestId('label')
  243. .map(el => el.textContent)
  244. ).toEqual(['count()']);
  245. const percentileRow = queryFields[2]!;
  246. expect(
  247. within(percentileRow)
  248. .getAllByTestId('label')
  249. .map(el => el.textContent)
  250. ).toEqual(['percentile(…)', 'transaction.duration']);
  251. expect(within(percentileRow).getByDisplayValue('0.66')).toBeInTheDocument();
  252. });
  253. });
  254. describe('function & column selection', function () {
  255. let onApply!: jest.Mock;
  256. beforeEach(function () {
  257. onApply = jest.fn();
  258. });
  259. it('restricts column choices', async function () {
  260. mountModal(
  261. {
  262. columns: [columns[0]!],
  263. onApply,
  264. customMeasurements: {},
  265. },
  266. initialData
  267. );
  268. await selectByLabel('avg(…)', {
  269. at: 0,
  270. });
  271. await openMenu(0, 1);
  272. const menuOptions = await screen.findAllByTestId('menu-list-item-label');
  273. const menuOptionsText = menuOptions.map(el => el.textContent);
  274. expect(menuOptionsText).toContain('transaction.duration');
  275. expect(menuOptionsText).not.toContain('title');
  276. });
  277. it('shows no options for parameterless functions', async function () {
  278. mountModal(
  279. {
  280. columns: [columns[0]!],
  281. onApply,
  282. customMeasurements: {},
  283. },
  284. initialData
  285. );
  286. await selectByLabel('last_seen()', {name: 'field', at: 0, control: true});
  287. expect(screen.getByTestId('blankSpace')).toBeInTheDocument();
  288. });
  289. it('shows additional inputs for multi-parameter functions', async function () {
  290. mountModal(
  291. {
  292. columns: [columns[0]!],
  293. onApply,
  294. customMeasurements: {},
  295. },
  296. initialData
  297. );
  298. await selectByLabel('percentile(\u2026)', {
  299. name: 'field',
  300. at: 0,
  301. });
  302. expect(screen.getAllByTestId('label')[0]).toHaveTextContent('percentile(…)');
  303. expect(
  304. within(screen.getByTestId('queryField')).getByDisplayValue(0.5)
  305. ).toBeInTheDocument();
  306. });
  307. it('handles scalar field parameters', async function () {
  308. mountModal(
  309. {
  310. columns: [columns[0]!],
  311. onApply,
  312. customMeasurements: {},
  313. },
  314. initialData
  315. );
  316. await selectByLabel('apdex(\u2026)', {
  317. name: 'field',
  318. at: 0,
  319. });
  320. expect(screen.getAllByRole('textbox')[1]).toHaveValue('300');
  321. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  322. await waitFor(() => {
  323. expect(onApply).toHaveBeenCalledWith([
  324. {kind: 'function', function: ['apdex', '300', undefined, undefined]},
  325. ]);
  326. });
  327. });
  328. it('handles parameter overrides', async function () {
  329. mountModal(
  330. {
  331. columns: [columns[0]!],
  332. onApply,
  333. customMeasurements: {},
  334. },
  335. initialData
  336. );
  337. await selectByLabel('apdex(…)', {
  338. name: 'field',
  339. at: 0,
  340. });
  341. expect(screen.getAllByRole('textbox')[1]).toHaveValue('300');
  342. });
  343. it('clears unused parameters', async function () {
  344. mountModal(
  345. {
  346. columns: [columns[0]!],
  347. onApply,
  348. customMeasurements: {},
  349. },
  350. initialData
  351. );
  352. // Choose percentile, then apdex which has fewer parameters and different types.
  353. await selectByLabel('percentile(\u2026)', {
  354. name: 'field',
  355. at: 0,
  356. });
  357. await selectByLabel('apdex(\u2026)', {
  358. name: 'field',
  359. at: 0,
  360. });
  361. // Apply the changes so we can see the new columns.
  362. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  363. expect(onApply).toHaveBeenCalledWith([
  364. {kind: 'function', function: ['apdex', '300', undefined, undefined]},
  365. ]);
  366. });
  367. it('clears all unused parameters', async function () {
  368. mountModal(
  369. {
  370. columns: [columns[0]!],
  371. onApply,
  372. customMeasurements: {},
  373. },
  374. initialData
  375. );
  376. // Choose percentile, then failure_rate which has no parameters.
  377. await selectByLabel('percentile(\u2026)', {
  378. name: 'field',
  379. at: 0,
  380. });
  381. await selectByLabel('failure_rate()', {
  382. name: 'field',
  383. at: 0,
  384. });
  385. // Apply the changes so we can see the new columns.
  386. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  387. expect(onApply).toHaveBeenCalledWith([
  388. {kind: 'function', function: ['failure_rate', '', undefined, undefined]},
  389. ]);
  390. });
  391. it('clears all unused parameters with count_if to two parameter function', async function () {
  392. mountModal(
  393. {
  394. columns: [columns[0]!],
  395. onApply,
  396. customMeasurements: {},
  397. },
  398. initialData
  399. );
  400. // Choose percentile, then failure_rate which has no parameters.
  401. await selectByLabel('count_if(\u2026)', {
  402. name: 'field',
  403. at: 0,
  404. });
  405. await selectByLabel('user', {name: 'parameter', at: 0});
  406. await selectByLabel('count_miserable(\u2026)', {
  407. name: 'field',
  408. at: 0,
  409. });
  410. // Apply the changes so we can see the new columns.
  411. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  412. expect(onApply).toHaveBeenCalledWith([
  413. {kind: 'function', function: ['count_miserable', 'user', '300', undefined]},
  414. ]);
  415. });
  416. it('clears all unused parameters with count_if to one parameter function', async function () {
  417. mountModal(
  418. {
  419. columns: [columns[0]!],
  420. onApply,
  421. customMeasurements: {},
  422. },
  423. initialData
  424. );
  425. // Choose percentile, then failure_rate which has no parameters.
  426. await selectByLabel('count_if(\u2026)', {
  427. name: 'field',
  428. at: 0,
  429. });
  430. await selectByLabel('user', {name: 'parameter', at: 0});
  431. await selectByLabel('count_unique(\u2026)', {
  432. name: 'field',
  433. at: 0,
  434. });
  435. // Apply the changes so we can see the new columns.
  436. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  437. expect(onApply).toHaveBeenCalledWith([
  438. {kind: 'function', function: ['count_unique', '300', undefined, undefined]},
  439. ]);
  440. });
  441. it('clears all unused parameters with count_if to parameterless function', async function () {
  442. mountModal(
  443. {
  444. columns: [columns[0]!],
  445. onApply,
  446. customMeasurements: {},
  447. },
  448. initialData
  449. );
  450. // Choose percentile, then failure_rate which has no parameters.
  451. await selectByLabel('count_if(\u2026)', {
  452. name: 'field',
  453. at: 0,
  454. });
  455. await selectByLabel('count()', {
  456. name: 'field',
  457. at: 0,
  458. });
  459. // Apply the changes so we can see the new columns.
  460. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  461. expect(onApply).toHaveBeenCalledWith([
  462. {kind: 'function', function: ['count', '', undefined, undefined]},
  463. ]);
  464. });
  465. it('updates equation errors when they change', async function () {
  466. mountModal(
  467. {
  468. columns: [
  469. {
  470. kind: 'equation',
  471. field: '1 / 0',
  472. },
  473. ],
  474. onApply,
  475. customMeasurements: {},
  476. },
  477. initialData
  478. );
  479. await userEvent.hover(await screen.findByTestId('arithmeticErrorWarning'));
  480. expect(await screen.findByText('Division by 0 is not allowed')).toBeInTheDocument();
  481. const input = screen.getAllByRole('textbox')[0]!;
  482. expect(input).toHaveValue('1 / 0');
  483. await userEvent.clear(input);
  484. await userEvent.type(input, '1+1+1+1+1+1+1+1+1+1+1+1');
  485. await userEvent.click(document.body);
  486. await waitFor(() => expect(input).toHaveValue('1+1+1+1+1+1+1+1+1+1+1+1'));
  487. await userEvent.hover(screen.getByTestId('arithmeticErrorWarning'));
  488. expect(await screen.findByText('Maximum operators exceeded')).toBeInTheDocument();
  489. });
  490. it('resets required field to previous value if cleared', async function () {
  491. const initialColumnVal = '0.6';
  492. mountModal(
  493. {
  494. columns: [
  495. {
  496. kind: 'function',
  497. function: [
  498. 'percentile',
  499. 'transaction.duration',
  500. initialColumnVal,
  501. undefined,
  502. ],
  503. },
  504. ],
  505. onApply,
  506. customMeasurements: {},
  507. },
  508. initialData
  509. );
  510. const input = screen.getAllByRole('textbox')[2]!; // The numeric input
  511. expect(input).toHaveValue(initialColumnVal);
  512. await userEvent.clear(input);
  513. await userEvent.click(document.body); // Unfocusing the input should revert it to the previous value
  514. expect(input).toHaveValue(initialColumnVal);
  515. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  516. expect(onApply).toHaveBeenCalledWith([
  517. {
  518. kind: 'function',
  519. function: ['percentile', 'transaction.duration', initialColumnVal, undefined],
  520. },
  521. ]);
  522. });
  523. it('chooses the correct default parameters for the errors dataset', async function () {
  524. mountModal(
  525. {
  526. columns: [
  527. {
  528. kind: 'function',
  529. function: ['count', '', undefined, undefined],
  530. },
  531. ],
  532. onApply,
  533. customMeasurements: {},
  534. dataset: DiscoverDatasets.ERRORS,
  535. },
  536. initialData
  537. );
  538. expect(await screen.findByText('count()')).toBeInTheDocument();
  539. await userEvent.click(screen.getByText('count()'));
  540. await userEvent.click(screen.getByText(/count_if/));
  541. expect(screen.getByText('event.type')).toBeInTheDocument();
  542. expect(screen.getByDisplayValue('error')).toBeInTheDocument();
  543. });
  544. it('chooses the correct default count_if parameters for the transactions dataset', async function () {
  545. mountModal(
  546. {
  547. columns: [
  548. {
  549. kind: 'function',
  550. function: ['count', '', undefined, undefined],
  551. },
  552. ],
  553. onApply,
  554. customMeasurements: {},
  555. dataset: DiscoverDatasets.TRANSACTIONS,
  556. },
  557. initialData
  558. );
  559. expect(await screen.findByText('count()')).toBeInTheDocument();
  560. await userEvent.click(screen.getByText('count()'));
  561. await userEvent.click(screen.getByText(/count_if/));
  562. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  563. expect(screen.getByDisplayValue('300')).toBeInTheDocument();
  564. });
  565. });
  566. describe('equation automatic update', function () {
  567. let onApply!: jest.Mock;
  568. beforeEach(function () {
  569. onApply = jest.fn();
  570. });
  571. it('update simple equation columns when they change', async function () {
  572. mountModal(
  573. {
  574. columns: [
  575. {
  576. kind: 'function',
  577. function: ['count_unique', 'user', undefined, undefined],
  578. },
  579. {
  580. kind: 'function',
  581. function: ['p95', '', undefined, undefined],
  582. },
  583. {
  584. kind: 'equation',
  585. field: '(p95() / count_unique(user) ) * 100',
  586. },
  587. ],
  588. onApply,
  589. customMeasurements: {},
  590. },
  591. initialData
  592. );
  593. await selectByLabel('count_if(\u2026)', {
  594. name: 'field',
  595. at: 0,
  596. });
  597. // Apply the changes so we can see the new columns.
  598. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  599. expect(onApply).toHaveBeenCalledWith([
  600. {kind: 'function', function: ['count_if', 'user', 'equals', '300']},
  601. {kind: 'function', function: ['p95', '']},
  602. {kind: 'equation', field: '(p95() / count_if(user,equals,300) ) * 100'},
  603. ]);
  604. });
  605. it('update equation with repeated columns when they change', async function () {
  606. mountModal(
  607. {
  608. columns: [
  609. {
  610. kind: 'function',
  611. function: ['count_unique', 'user', undefined, undefined],
  612. },
  613. {
  614. kind: 'equation',
  615. field:
  616. 'count_unique(user) + (count_unique(user) - count_unique(user)) * 5',
  617. },
  618. ],
  619. onApply,
  620. customMeasurements: {},
  621. },
  622. initialData
  623. );
  624. await selectByLabel('count()', {
  625. name: 'field',
  626. at: 0,
  627. });
  628. // Apply the changes so we can see the new columns.
  629. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  630. expect(onApply).toHaveBeenCalledWith([
  631. {kind: 'function', function: ['count', '', undefined, undefined]},
  632. {kind: 'equation', field: 'count() + (count() - count()) * 5'},
  633. ]);
  634. });
  635. it('handles equations with duplicate fields', async function () {
  636. mountModal(
  637. {
  638. columns: [
  639. {
  640. kind: 'field',
  641. field: 'spans.db',
  642. },
  643. {
  644. kind: 'field',
  645. field: 'spans.db',
  646. },
  647. {
  648. kind: 'equation',
  649. field: 'spans.db - spans.db',
  650. },
  651. ],
  652. onApply,
  653. customMeasurements: {},
  654. },
  655. initialData
  656. );
  657. await selectByLabel('count()', {
  658. name: 'field',
  659. at: 0,
  660. });
  661. // Apply the changes so we can see the new columns.
  662. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  663. // Because spans.db is still a selected column it isn't swapped
  664. expect(onApply).toHaveBeenCalledWith([
  665. {kind: 'function', function: ['count', '', undefined, undefined]},
  666. {kind: 'field', field: 'spans.db'},
  667. {kind: 'equation', field: 'spans.db - spans.db'},
  668. ]);
  669. });
  670. it('handles equations with duplicate functions', async function () {
  671. mountModal(
  672. {
  673. columns: [
  674. {
  675. kind: 'function',
  676. function: ['count', '', undefined, undefined],
  677. },
  678. {
  679. kind: 'function',
  680. function: ['count', '', undefined, undefined],
  681. },
  682. {
  683. kind: 'equation',
  684. field: 'count() - count()',
  685. },
  686. ],
  687. onApply,
  688. customMeasurements: {},
  689. },
  690. initialData
  691. );
  692. await selectByLabel('count_unique(\u2026)', {
  693. name: 'field',
  694. at: 0,
  695. });
  696. // Apply the changes so we can see the new columns.
  697. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  698. expect(onApply).toHaveBeenCalledWith([
  699. {kind: 'function', function: ['count_unique', 'user', undefined, undefined]},
  700. {kind: 'function', function: ['count', '', undefined, undefined]},
  701. {kind: 'equation', field: 'count() - count()'},
  702. ]);
  703. });
  704. it('handles incomplete equations', async function () {
  705. mountModal(
  706. {
  707. columns: [
  708. {
  709. kind: 'function',
  710. function: ['count', '', undefined, undefined],
  711. },
  712. {
  713. kind: 'equation',
  714. field: 'count() - count() arst count() ',
  715. },
  716. ],
  717. onApply,
  718. customMeasurements: {},
  719. },
  720. initialData
  721. );
  722. expect(await screen.findByTestId('arithmeticErrorWarning')).toBeInTheDocument();
  723. await selectByLabel('count_unique(\u2026)', {
  724. name: 'field',
  725. at: 0,
  726. });
  727. // Apply the changes so we can see the new columns.
  728. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  729. // With the way the parser works only tokens up to the error will be updated
  730. expect(onApply).toHaveBeenCalledWith([
  731. {kind: 'function', function: ['count_unique', 'user', undefined, undefined]},
  732. {
  733. kind: 'equation',
  734. field: 'count_unique(user) - count_unique(user) arst count() ',
  735. },
  736. ]);
  737. });
  738. });
  739. describe('adding rows', function () {
  740. it('allows rows to be added, async but only up to 20', async function () {
  741. mountModal(
  742. {
  743. columns: [columns[0]!],
  744. onApply: jest.fn(),
  745. customMeasurements: {},
  746. },
  747. initialData
  748. );
  749. expect(await screen.findByTestId('queryField')).toBeInTheDocument();
  750. const addColumnButton = screen.getByRole('button', {name: 'Add a Column'});
  751. for (let i = 2; i <= 20; i++) {
  752. fireEvent.click(addColumnButton);
  753. expect(await screen.findAllByTestId('queryField')).toHaveLength(i);
  754. }
  755. expect(screen.getByRole('button', {name: 'Add a Column'})).toBeDisabled();
  756. });
  757. });
  758. describe('removing rows', function () {
  759. it('allows rows to be removed, async but not the last one', async function () {
  760. mountModal(
  761. {
  762. columns: [columns[0]!, columns[1]!],
  763. onApply: jest.fn(),
  764. customMeasurements: {},
  765. },
  766. initialData
  767. );
  768. expect(await screen.findAllByTestId('queryField')).toHaveLength(2);
  769. await userEvent.click(screen.getByTestId('remove-column-0'));
  770. expect(await screen.findByTestId('queryField')).toBeInTheDocument();
  771. expect(
  772. screen.queryByRole('button', {name: 'Remove column'})
  773. ).not.toBeInTheDocument();
  774. expect(
  775. screen.queryByRole('button', {name: 'Drag to reorder'})
  776. ).not.toBeInTheDocument();
  777. });
  778. it('does not count equations towards the count of rows', async function () {
  779. mountModal(
  780. {
  781. columns: [
  782. columns[0]!,
  783. columns[1]!,
  784. {
  785. kind: 'equation',
  786. field: '5 + 5',
  787. },
  788. ],
  789. onApply: jest.fn(),
  790. customMeasurements: {},
  791. },
  792. initialData
  793. );
  794. expect(await screen.findAllByTestId('queryField')).toHaveLength(3);
  795. await userEvent.click(screen.getByTestId('remove-column-0'));
  796. expect(await screen.findAllByTestId('queryField')).toHaveLength(2);
  797. expect(screen.getByRole('button', {name: 'Remove column'})).toBeInTheDocument();
  798. expect(screen.queryAllByRole('button', {name: 'Drag to reorder'})).toHaveLength(2);
  799. });
  800. it('handles equations being deleted', async function () {
  801. mountModal(
  802. {
  803. columns: [
  804. {
  805. kind: 'equation',
  806. field: '1 / 0',
  807. },
  808. columns[0]!,
  809. columns[1]!,
  810. ],
  811. onApply: jest.fn(),
  812. customMeasurements: {},
  813. },
  814. initialData
  815. );
  816. expect(await screen.findAllByTestId('queryField')).toHaveLength(3);
  817. expect(screen.getByTestId('arithmeticErrorWarning')).toBeInTheDocument();
  818. await userEvent.click(screen.getByTestId('remove-column-0'));
  819. expect(await screen.findAllByTestId('queryField')).toHaveLength(2);
  820. expect(screen.queryByTestId('arithmeticErrorWarning')).not.toBeInTheDocument();
  821. });
  822. });
  823. describe('apply action', function () {
  824. const onApply = jest.fn();
  825. it('reflects added and removed columns', async function () {
  826. mountModal(
  827. {
  828. columns: [columns[0]!, columns[1]!],
  829. onApply,
  830. customMeasurements: {},
  831. },
  832. initialData
  833. );
  834. expect(await screen.findAllByTestId('queryField')).toHaveLength(2);
  835. // Remove a column, then add a blank one an select a value in it.
  836. await userEvent.click(screen.getByTestId('remove-column-0'));
  837. await userEvent.click(screen.getByRole('button', {name: 'Add a Column'}));
  838. expect(await screen.findAllByTestId('queryField')).toHaveLength(2);
  839. await selectByLabel('title', {name: 'field', at: 1});
  840. await userEvent.click(screen.getByRole('button', {name: 'Apply'}));
  841. expect(onApply).toHaveBeenCalledWith([columns[1], {kind: 'field', field: 'title'}]);
  842. });
  843. });
  844. describe('custom performance metrics', function () {
  845. it('allows selecting custom performance metrics in dropdown', async function () {
  846. render(
  847. <ColumnEditModal
  848. CloseButton={makeCloseButton(() => {})}
  849. Header={c => <div>{c.children}</div>}
  850. Footer={stubEl()}
  851. Body={stubEl()}
  852. organization={initialData.organization}
  853. columns={[columns[0]!]}
  854. onApply={() => undefined}
  855. closeModal={() => undefined}
  856. measurementKeys={[]}
  857. customMeasurements={{
  858. 'measurements.custom.kibibyte': {
  859. fieldType: 'number',
  860. unit: 'KiB',
  861. key: 'measurements.custom.kibibyte',
  862. name: 'measurements.custom.kibibyte',
  863. functions: ['p99'],
  864. },
  865. 'measurements.custom.minute': {
  866. fieldType: 'number',
  867. key: 'measurements.custom.minute',
  868. name: 'measurements.custom.minute',
  869. unit: 'minute',
  870. functions: ['p99'],
  871. },
  872. 'measurements.custom.ratio': {
  873. fieldType: 'number',
  874. key: 'measurements.custom.ratio',
  875. name: 'measurements.custom.ratio',
  876. unit: 'ratio',
  877. functions: ['p99'],
  878. },
  879. }}
  880. />
  881. );
  882. expect(screen.getByText('event.type')).toBeInTheDocument();
  883. await userEvent.click(screen.getByText('event.type'));
  884. await userEvent.type(screen.getAllByText('event.type')[0]!, 'custom');
  885. expect(screen.getByText('measurements.custom.kibibyte')).toBeInTheDocument();
  886. });
  887. });
  888. });