columnEditModal.spec.jsx 25 KB

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