modal.spec.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import {
  2. screen,
  3. userEvent,
  4. waitForElementToBeRemoved,
  5. within,
  6. } from 'sentry-test/reactTestingLibrary';
  7. import {textWithMarkupMatcher} from 'sentry-test/utils';
  8. import {SamplingInnerName, SamplingRuleType} from 'sentry/types/sampling';
  9. import {
  10. distributedTracesConditions,
  11. individualTransactionsConditions,
  12. } from 'sentry/views/settings/project/sampling/modal/utils';
  13. import {
  14. getInnerNameLabel,
  15. LEGACY_BROWSER_LIST,
  16. } from 'sentry/views/settings/project/sampling/utils';
  17. import {openSamplingRuleModal, renderComponent} from './utils';
  18. describe('Sampling - Modal', function () {
  19. beforeEach(function () {
  20. MockApiClient.addMockResponse({
  21. url: '/projects/org-slug/project-slug/',
  22. method: 'GET',
  23. body: TestStubs.Project(),
  24. });
  25. MockApiClient.addMockResponse({
  26. url: '/projects/org-slug/project-slug/tags/',
  27. body: TestStubs.Tags,
  28. });
  29. MockApiClient.addMockResponse({
  30. url: '/organizations/org-slug/tags/release/values/',
  31. method: 'GET',
  32. body: [{value: '1.2.3'}],
  33. });
  34. });
  35. afterEach(function () {
  36. MockApiClient.clearMockResponses();
  37. });
  38. it('saves distributed traces rule', async function () {
  39. const rule = {
  40. condition: {
  41. inner: [{name: 'trace.release', op: 'glob', value: ['1.2.3']}],
  42. op: 'and',
  43. },
  44. id: 0,
  45. sampleRate: 0.2,
  46. type: 'trace',
  47. };
  48. const saveMock = MockApiClient.addMockResponse({
  49. url: '/projects/org-slug/project-slug/',
  50. method: 'PUT',
  51. body: TestStubs.Project({
  52. dynamicSampling: {
  53. rules: [rule],
  54. next_id: 41,
  55. },
  56. }),
  57. });
  58. renderComponent({ruleType: SamplingRuleType.TRACE});
  59. // Open Modal
  60. await openSamplingRuleModal(screen.getByText('Add Rule'));
  61. const dialog = screen.getByRole('dialog');
  62. // Modal description
  63. expect(
  64. screen.getByText(
  65. textWithMarkupMatcher(
  66. 'Using a Trace ID, select all Transactions distributed across multiple projects/services which match your conditions. However, if you only want to select Transactions from within this project, we recommend you add a Individual Transaction rule instead.'
  67. )
  68. )
  69. ).toBeInTheDocument();
  70. expect(screen.getByRole('link', {name: 'Individual Transaction'})).toHaveAttribute(
  71. 'href',
  72. `${SamplingRuleType.TRANSACTION}/`
  73. );
  74. // Empty conditions message
  75. expect(screen.getByText('No conditions added')).toBeInTheDocument();
  76. expect(
  77. screen.getByText(
  78. textWithMarkupMatcher(
  79. "if you don't want to add (+) a condition, simply, add a sample rate below"
  80. )
  81. )
  82. ).toBeInTheDocument();
  83. // Click on 'Add condition'
  84. userEvent.click(screen.getByText('Add Condition'));
  85. // Autocomplete
  86. expect(screen.getByText(/filter conditions/i)).toBeInTheDocument();
  87. // Distributed Traces Options
  88. distributedTracesConditions.forEach(condition => {
  89. expect(within(dialog).getByText(getInnerNameLabel(condition))).toBeInTheDocument();
  90. });
  91. expect(
  92. within(dialog).queryByText(
  93. getInnerNameLabel(SamplingInnerName.EVENT_LEGACY_BROWSER)
  94. )
  95. ).not.toBeInTheDocument();
  96. // Click on the condition option
  97. userEvent.click(screen.getAllByText('Release')[0]);
  98. // Release field is empty
  99. expect(screen.queryByTestId('multivalue')).not.toBeInTheDocument();
  100. // Type into release field
  101. userEvent.paste(screen.getByLabelText('Search or add a release'), '1.2.3');
  102. // Autocomplete suggests options
  103. expect(screen.getByTestId('1.2.3')).toHaveTextContent('1.2.3');
  104. // Click on the suggested option
  105. userEvent.click(screen.getByTestId('1.2.3'));
  106. // Button is still disabled
  107. expect(screen.getByLabelText('Save Rule')).toBeDisabled();
  108. // Fill sample rate field
  109. userEvent.paste(screen.getByPlaceholderText('\u0025'), '20');
  110. // Save button is now enabled
  111. expect(screen.getByLabelText('Save Rule')).toBeEnabled();
  112. // Click on save button
  113. userEvent.click(screen.getByLabelText('Save Rule'));
  114. // Close Modal
  115. userEvent.click(screen.getByLabelText('Close Modal'));
  116. await waitForElementToBeRemoved(() =>
  117. screen.queryByText('Add Distributed Trace Rule')
  118. );
  119. expect(saveMock).toHaveBeenLastCalledWith(
  120. '/projects/org-slug/project-slug/',
  121. expect.objectContaining({
  122. data: {
  123. dynamicSampling: {
  124. rules: [rule],
  125. },
  126. },
  127. })
  128. );
  129. expect(screen.getByText('Release')).toBeInTheDocument();
  130. expect(screen.getByText('1.2.3')).toBeInTheDocument();
  131. expect(screen.getByText('20%')).toBeInTheDocument();
  132. });
  133. it('saves individual transactions rule', async function () {
  134. const rule = {
  135. condition: {
  136. inner: [{name: 'event.release', op: 'glob', value: ['1.2.3']}],
  137. op: 'and',
  138. },
  139. id: 0,
  140. sampleRate: 0.3,
  141. type: 'transaction',
  142. };
  143. const saveMock = MockApiClient.addMockResponse({
  144. url: '/projects/org-slug/project-slug/',
  145. method: 'PUT',
  146. body: TestStubs.Project({
  147. dynamicSampling: {
  148. rules: [rule],
  149. next_id: 41,
  150. },
  151. }),
  152. });
  153. renderComponent({ruleType: SamplingRuleType.TRANSACTION});
  154. // Open Modal
  155. await openSamplingRuleModal(screen.getByText('Add Rule'));
  156. const dialog = screen.getByRole('dialog');
  157. // Modal description
  158. expect(
  159. screen.getByText(
  160. textWithMarkupMatcher(
  161. 'Select Transactions only within this project which match your conditions. However, If you want to select all Transactions distributed across multiple projects/services, we recommend you add a Distributed Trace rule instead.'
  162. )
  163. )
  164. ).toBeInTheDocument();
  165. expect(screen.getByRole('link', {name: 'Distributed Trace'})).toHaveAttribute(
  166. 'href',
  167. `${SamplingRuleType.TRACE}/`
  168. );
  169. // Empty conditions message
  170. expect(screen.getByText('No conditions added')).toBeInTheDocument();
  171. expect(
  172. screen.getByText(
  173. textWithMarkupMatcher(
  174. "if you don't want to add (+) a condition, simply, add a sample rate below"
  175. )
  176. )
  177. ).toBeInTheDocument();
  178. // Click on 'Add condition'
  179. userEvent.click(screen.getByText('Add Condition'));
  180. // Individual Transactions Options
  181. individualTransactionsConditions.forEach(condition => {
  182. expect(within(dialog).getByText(getInnerNameLabel(condition))).toBeInTheDocument();
  183. });
  184. // Click on the first condition option
  185. userEvent.click(screen.getAllByText('Release')[0]);
  186. // Type into release field
  187. userEvent.paste(screen.getByLabelText('Search or add a release'), '1.2.3');
  188. // Click on the suggested option
  189. userEvent.click(screen.getByTestId('1.2.3'));
  190. // Fill sample rate field
  191. userEvent.paste(screen.getByPlaceholderText('\u0025'), '30');
  192. // Click on save button
  193. userEvent.click(screen.getByLabelText('Save Rule'));
  194. // Close Modal
  195. userEvent.click(screen.getByLabelText('Close Modal'));
  196. await waitForElementToBeRemoved(() =>
  197. screen.queryByText('Add Individual Transaction Rule')
  198. );
  199. expect(saveMock).toHaveBeenLastCalledWith(
  200. '/projects/org-slug/project-slug/',
  201. expect.objectContaining({
  202. data: {
  203. dynamicSampling: {
  204. rules: [rule],
  205. },
  206. },
  207. })
  208. );
  209. });
  210. it('edits the rule', async function () {
  211. const newRule = {
  212. condition: {
  213. inner: [{name: 'trace.release', op: 'glob', value: ['1.2.3']}],
  214. op: 'and',
  215. },
  216. id: 0,
  217. sampleRate: 0.6,
  218. type: 'trace',
  219. };
  220. MockApiClient.addMockResponse({
  221. url: '/projects/org-slug/project-slug/',
  222. method: 'GET',
  223. body: TestStubs.Project({
  224. dynamicSampling: {
  225. rules: [
  226. {
  227. sampleRate: 0.2,
  228. type: 'trace',
  229. condition: {
  230. op: 'and',
  231. inner: [
  232. {
  233. op: 'glob',
  234. name: 'trace.release',
  235. value: ['1.2.2'],
  236. },
  237. ],
  238. },
  239. id: 40,
  240. },
  241. ],
  242. next_id: 41,
  243. },
  244. }),
  245. });
  246. const saveMock = MockApiClient.addMockResponse({
  247. url: '/projects/org-slug/project-slug/',
  248. method: 'PUT',
  249. body: TestStubs.Project({
  250. dynamicSampling: {
  251. rules: [newRule],
  252. next_id: 42,
  253. },
  254. }),
  255. });
  256. renderComponent();
  257. expect(screen.getByText('1.2.2')).toBeInTheDocument();
  258. expect(screen.getByText('20%')).toBeInTheDocument();
  259. await openSamplingRuleModal(screen.getByLabelText('Edit Rule'));
  260. // Empty conditions message is not displayed
  261. expect(screen.queryByText('No conditions added')).not.toBeInTheDocument();
  262. // Type into realease field
  263. userEvent.clear(screen.getByLabelText('Search or add a release'));
  264. userEvent.paste(screen.getByLabelText('Search or add a release'), '1.2.3');
  265. // Click on the suggested option
  266. userEvent.click(await screen.findByText(textWithMarkupMatcher('Add "1.2.3"')));
  267. // Update sample rate field
  268. userEvent.clear(screen.getByPlaceholderText('\u0025'));
  269. userEvent.paste(screen.getByPlaceholderText('\u0025'), '60');
  270. // Click on save button
  271. userEvent.click(screen.getByLabelText('Save Rule'));
  272. // Modal will close
  273. await waitForElementToBeRemoved(() =>
  274. screen.queryByText('Edit Distributed Trace Rule')
  275. );
  276. expect(saveMock).toHaveBeenLastCalledWith(
  277. '/projects/org-slug/project-slug/',
  278. expect.objectContaining({
  279. data: {
  280. dynamicSampling: {
  281. rules: [newRule],
  282. },
  283. },
  284. })
  285. );
  286. // Old values
  287. expect(screen.queryByText('1.2.2')).not.toBeInTheDocument();
  288. expect(screen.queryByText('20%')).not.toBeInTheDocument();
  289. // New values
  290. expect(screen.getByText('1.2.3')).toBeInTheDocument();
  291. expect(screen.getByText('60%')).toBeInTheDocument();
  292. });
  293. it('legacy browsers condition', async function () {
  294. const rule = {
  295. condition: {
  296. inner: [
  297. {
  298. name: 'event.legacy_browser',
  299. op: 'custom',
  300. value: [
  301. 'ie_pre_9',
  302. 'ie9',
  303. 'ie10',
  304. 'ie11',
  305. 'safari_pre_6',
  306. 'opera_pre_15',
  307. 'opera_mini_pre_8',
  308. 'android_pre_4',
  309. ],
  310. },
  311. ],
  312. op: 'and',
  313. },
  314. id: 0,
  315. sampleRate: 0.2,
  316. type: 'transaction',
  317. };
  318. const saveMock = MockApiClient.addMockResponse({
  319. url: '/projects/org-slug/project-slug/',
  320. method: 'PUT',
  321. body: TestStubs.Project({
  322. dynamicSampling: {
  323. rules: [rule],
  324. next_id: 43,
  325. },
  326. }),
  327. });
  328. renderComponent({ruleType: SamplingRuleType.TRANSACTION});
  329. // Open Modal
  330. await openSamplingRuleModal(screen.getByText('Add Rule'));
  331. // Click on 'Add condition'
  332. userEvent.click(screen.getByText('Add Condition'));
  333. // Select Legacy Browser
  334. userEvent.click(screen.getByText('Legacy Browser'));
  335. // Legacy Browsers
  336. expect(screen.getByText('All browsers')).toBeInTheDocument();
  337. const legacyBrowsers = Object.keys(LEGACY_BROWSER_LIST);
  338. for (const legacyBrowser of legacyBrowsers) {
  339. const {icon, title} = LEGACY_BROWSER_LIST[legacyBrowser];
  340. expect(screen.getByText(title)).toBeInTheDocument();
  341. expect(screen.getAllByTestId(`icon-${icon}`)[0]).toBeInTheDocument();
  342. }
  343. expect(screen.getAllByTestId('icon-internet-explorer')).toHaveLength(4);
  344. expect(screen.getAllByTestId('icon-opera')).toHaveLength(2);
  345. expect(screen.getByTestId('icon-safari')).toBeInTheDocument();
  346. expect(screen.getByTestId('icon-android')).toBeInTheDocument();
  347. const switchButtons = screen.getAllByTestId('switch');
  348. expect(switchButtons).toHaveLength(legacyBrowsers.length + 1);
  349. // All browsers are unchecked
  350. for (const switchButton of switchButtons) {
  351. expect(switchButton).not.toBeChecked();
  352. }
  353. // Click on the switch of 'All browsers' option
  354. userEvent.click(switchButtons[0]);
  355. // All browsers are now checked
  356. for (const switchButton of switchButtons) {
  357. expect(switchButton).toBeChecked();
  358. }
  359. // Fill sample rate field
  360. userEvent.paste(screen.getByPlaceholderText('\u0025'), '20');
  361. // Click on save button
  362. userEvent.click(screen.getByLabelText('Save Rule'));
  363. // Close Modal
  364. userEvent.click(screen.getByLabelText('Close Modal'));
  365. await waitForElementToBeRemoved(() =>
  366. screen.queryByText('Add Individual Transaction Rule')
  367. );
  368. expect(saveMock).toHaveBeenLastCalledWith(
  369. '/projects/org-slug/project-slug/',
  370. expect.objectContaining({
  371. data: {
  372. dynamicSampling: {
  373. rules: [rule],
  374. },
  375. },
  376. })
  377. );
  378. // Transaction rules panel is updated
  379. expect(screen.getByText('Legacy Browser')).toBeInTheDocument();
  380. for (const legacyBrowser of legacyBrowsers) {
  381. const {title} = LEGACY_BROWSER_LIST[legacyBrowser];
  382. expect(screen.getByText(title)).toBeInTheDocument();
  383. }
  384. });
  385. it('custom tag condition', async function () {
  386. const rule = {
  387. condition: {
  388. inner: [{name: 'event.tags.user', op: 'glob', value: ['david']}],
  389. op: 'and',
  390. },
  391. id: 0,
  392. sampleRate: 0.15,
  393. type: 'transaction',
  394. };
  395. MockApiClient.addMockResponse({
  396. url: '/organizations/org-slug/tags/user/values/',
  397. method: 'GET',
  398. body: [{value: 'david'}],
  399. });
  400. const saveMock = MockApiClient.addMockResponse({
  401. url: '/projects/org-slug/project-slug/',
  402. method: 'PUT',
  403. body: TestStubs.Project({
  404. dynamicSampling: {
  405. rules: [rule],
  406. next_id: 43,
  407. },
  408. }),
  409. });
  410. renderComponent({ruleType: SamplingRuleType.TRANSACTION});
  411. // Open Modal
  412. await openSamplingRuleModal(screen.getByText('Add Rule'));
  413. // Click on 'Add condition'
  414. userEvent.click(screen.getByText('Add Condition'));
  415. // Select Custom Tag
  416. userEvent.click(screen.getByText('Add Custom Tag'));
  417. // Type into tag field
  418. userEvent.paste(screen.getByLabelText('Search or add a tag'), 'user');
  419. // Click on the suggested option
  420. userEvent.click(screen.getByTestId('user'));
  421. // Type into tag value field
  422. userEvent.paste(screen.getByLabelText('Search or add tag values'), 'david');
  423. // Click on the suggested option
  424. userEvent.click(screen.getByTestId('david'));
  425. // Fill sample rate field
  426. userEvent.paste(screen.getByPlaceholderText('\u0025'), '15');
  427. // Click on 'Add condition'
  428. userEvent.click(screen.getByText('Add Condition'));
  429. // Custom tag condition is added to the conditions dropdown
  430. expect(screen.getByText('user - Custom')).toBeInTheDocument();
  431. // Click on save button
  432. userEvent.click(screen.getByLabelText('Save Rule'));
  433. // Close Modal
  434. userEvent.click(screen.getByLabelText('Close Modal'));
  435. await waitForElementToBeRemoved(() =>
  436. screen.queryByText('Add Individual Transaction Rule')
  437. );
  438. expect(saveMock).toHaveBeenLastCalledWith(
  439. '/projects/org-slug/project-slug/',
  440. expect.objectContaining({
  441. data: {
  442. dynamicSampling: {
  443. rules: [rule],
  444. },
  445. },
  446. })
  447. );
  448. });
  449. it('invalid custom tag condition', async function () {
  450. MockApiClient.addMockResponse({
  451. url: '/organizations/org-slug/tags/sentry.key/values/',
  452. method: 'GET',
  453. body: [],
  454. });
  455. renderComponent({ruleType: SamplingRuleType.TRANSACTION});
  456. // Open Modal
  457. await openSamplingRuleModal(screen.getByText('Add Rule'));
  458. // Click on 'Add condition'
  459. userEvent.click(screen.getByText('Add Condition'));
  460. // Select Custom Tag
  461. userEvent.click(screen.getByText('Add Custom Tag'));
  462. // Type invalid value into tag field
  463. userEvent.paste(screen.getByLabelText('Search or add a tag'), 'sentry.*');
  464. // Dropdown display 'no options' because the tag is invalid
  465. expect(await screen.findByText('No options')).toBeInTheDocument();
  466. // Type valid value into tag field
  467. userEvent.type(screen.getByLabelText('Search or add a tag'), '{backspace}key');
  468. // Click on the suggested option
  469. userEvent.click(screen.getByTestId('sentry.key'));
  470. // Type invalid value into tag value field
  471. userEvent.paste(screen.getByLabelText('Search or add tag values'), 'invalid\\nvalue');
  472. // Dropdown display 'no options' because the tag value is invalid
  473. expect(await screen.findByText('No options')).toBeInTheDocument();
  474. // Clears tag value field
  475. userEvent.clear(screen.getByLabelText('Search or add tag values'));
  476. // Type valid value into tag value field
  477. userEvent.paste(screen.getByLabelText('Search or add tag values'), 'valid');
  478. // Click on the suggested option
  479. userEvent.click(screen.getByTestId('valid'));
  480. });
  481. it('does not let you save without permissions', async function () {
  482. renderComponent({
  483. orgOptions: {
  484. features: ['filters-and-sampling'],
  485. access: [],
  486. },
  487. });
  488. // Open Modal
  489. await openSamplingRuleModal(screen.getByText('Add Rule'));
  490. // Click on 'Add condition'
  491. userEvent.click(screen.getByText('Add Condition'));
  492. // Click on the condition option
  493. userEvent.click(screen.getAllByText('Release')[0]);
  494. // Type into release field
  495. userEvent.paste(screen.getByLabelText('Search or add a release'), '1.2.3');
  496. // Click on the suggested option
  497. userEvent.click(screen.getByTestId('1.2.3'));
  498. // Fill sample rate field
  499. userEvent.paste(screen.getByPlaceholderText('\u0025'), '20');
  500. // Button is still disabled
  501. expect(screen.getByLabelText('Save Rule')).toBeDisabled();
  502. // Close Modal
  503. userEvent.click(screen.getByLabelText('Close Modal'));
  504. await waitForElementToBeRemoved(() =>
  505. screen.queryByText('Add Distributed Trace Rule')
  506. );
  507. });
  508. it('does not let you save a distributed trace rule without a condition, if a trace rule without a condition already exists', async function () {
  509. MockApiClient.addMockResponse({
  510. url: '/projects/org-slug/project-slug/',
  511. method: 'GET',
  512. body: TestStubs.Project({
  513. dynamicSampling: {
  514. rules: [
  515. {
  516. sampleRate: 0.2,
  517. type: 'trace',
  518. condition: {
  519. op: 'and',
  520. inner: [
  521. {
  522. op: 'glob',
  523. name: 'trace.release',
  524. value: ['1.2.2'],
  525. },
  526. ],
  527. },
  528. id: 40,
  529. },
  530. {
  531. sampleRate: 0.5,
  532. type: 'trace',
  533. condition: {
  534. op: 'and',
  535. inner: [],
  536. },
  537. id: 41,
  538. },
  539. ],
  540. next_id: 42,
  541. },
  542. }),
  543. });
  544. renderComponent();
  545. // Open Modal
  546. await openSamplingRuleModal(screen.getByText('Add Rule'));
  547. // Empty conditions message
  548. expect(screen.getByText('No conditions added')).toBeInTheDocument();
  549. // A hint about an existing 'sample all' rule is displayed
  550. expect(
  551. screen.getByText(
  552. 'A rule with no conditions already exists. You can edit that existing rule or add a condition to this rule'
  553. )
  554. ).toBeInTheDocument();
  555. // Adds a sample rate
  556. userEvent.paste(screen.getByPlaceholderText('\u0025'), '5');
  557. // Button is still disabled
  558. expect(screen.getByLabelText('Save Rule')).toBeDisabled();
  559. });
  560. it('does not let you save a individual transaction rule without a condition, if a transaction rule without a condition already exists', async function () {
  561. MockApiClient.addMockResponse({
  562. url: '/projects/org-slug/project-slug/',
  563. method: 'GET',
  564. body: TestStubs.Project({
  565. dynamicSampling: {
  566. rules: [
  567. {
  568. condition: {
  569. inner: [{name: 'event.release', op: 'glob', value: ['1.2.3']}],
  570. op: 'and',
  571. },
  572. id: 0,
  573. sampleRate: 0.3,
  574. type: 'transaction',
  575. },
  576. {
  577. condition: {
  578. inner: [],
  579. op: 'and',
  580. },
  581. id: 1,
  582. sampleRate: 0.2,
  583. type: 'transaction',
  584. },
  585. ],
  586. next_id: 2,
  587. },
  588. }),
  589. });
  590. renderComponent({ruleType: SamplingRuleType.TRANSACTION});
  591. // Open Modal
  592. await openSamplingRuleModal(screen.getByText('Add Rule'));
  593. // Empty conditions message
  594. expect(screen.getByText('No conditions added')).toBeInTheDocument();
  595. // A hint about an existing 'sample all' rule is displayed
  596. expect(
  597. screen.getByText(
  598. 'A rule with no conditions already exists. You can edit that existing rule or add a condition to this rule'
  599. )
  600. ).toBeInTheDocument();
  601. // Adds a sample rate
  602. userEvent.paste(screen.getByPlaceholderText('\u0025'), '5');
  603. // Button is still disabled
  604. expect(screen.getByLabelText('Save Rule')).toBeDisabled();
  605. });
  606. });