columnEditModal.spec.tsx 29 KB

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