columnEditModal.spec.tsx 27 KB

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