columnEditModal.spec.jsx 25 KB

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