index.spec.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. import {ProjectFixture} from 'getsentry-test/fixtures/project';
  2. import {
  3. mockRootAllocations,
  4. mockSpendAllocations,
  5. } from 'getsentry-test/fixtures/spendAllocation';
  6. import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
  7. import {initializeOrg} from 'sentry-test/initializeOrg';
  8. import {
  9. act,
  10. cleanup,
  11. render,
  12. renderGlobalModal,
  13. screen,
  14. userEvent,
  15. waitFor,
  16. within,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import selectEvent from 'sentry-test/selectEvent';
  19. import ProjectsStore from 'sentry/stores/projectsStore';
  20. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  21. import {SpendAllocationsRoot} from 'getsentry/views/spendAllocations/index';
  22. describe('SpendAllocations feature enable flow', () => {
  23. let organization: any, subscription: any, mockGet: any, dateTs: number;
  24. beforeEach(() => {
  25. organization = initializeOrg({
  26. organization: {
  27. features: ['spend-allocations'],
  28. },
  29. }).organization;
  30. subscription = SubscriptionFixture({
  31. organization,
  32. plan: 'am1_f',
  33. planTier: 'am1',
  34. });
  35. MockApiClient.clearMockResponses();
  36. dateTs = Math.max(
  37. new Date().getTime() / 1000,
  38. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  39. );
  40. mockGet = MockApiClient.addMockResponse({
  41. url: `/organizations/${organization.slug}/spend-allocations/`,
  42. method: 'GET',
  43. body: [],
  44. status: 403,
  45. statusCode: 403,
  46. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  47. });
  48. });
  49. afterEach(() => {
  50. cleanup();
  51. });
  52. it('renders enable button for owners/billing in an org that has not enabled spend allocations', async () => {
  53. organization.access = [
  54. 'org:read',
  55. 'org:write',
  56. 'org:admin',
  57. 'org:billing',
  58. 'project:read',
  59. 'project:admin',
  60. ];
  61. render(
  62. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  63. );
  64. await waitFor(() =>
  65. screen.findByRole('button', {
  66. name: 'Get started',
  67. })
  68. );
  69. const enableSpendAllocations = screen.getByRole('button', {
  70. name: 'Get started',
  71. });
  72. expect(enableSpendAllocations).toBeInTheDocument();
  73. expect(enableSpendAllocations).toBeEnabled();
  74. });
  75. it('does not render for YY partnership', async function () {
  76. subscription = SubscriptionFixture({
  77. plan: 'am2_business',
  78. planTier: 'am2',
  79. partner: {
  80. externalId: 'x123x',
  81. name: 'YY Org',
  82. partnership: {
  83. id: 'YY',
  84. displayName: 'YY',
  85. supportNote: 'foo',
  86. },
  87. isActive: true,
  88. },
  89. organization,
  90. });
  91. SubscriptionStore.set(organization.slug, subscription);
  92. render(
  93. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  94. );
  95. expect(await screen.findByTestId('partnership-note')).toBeInTheDocument();
  96. expect(screen.queryByRole('button', {name: 'Get Started'})).not.toBeInTheDocument();
  97. });
  98. it('does not render enable button for non billing role for org that has not enabled spend allocations', async () => {
  99. organization.access = ['project:read', 'project:admin'];
  100. render(
  101. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  102. );
  103. // Waiting a tick for requests to finish
  104. await act(tick);
  105. expect(screen.queryByRole('button', {name: 'Get Started'})).not.toBeInTheDocument();
  106. });
  107. it('re-fetches org and project spend-allocations on enable click', async () => {
  108. organization.access = [
  109. 'org:read',
  110. 'org:write',
  111. 'org:admin',
  112. 'org:billing',
  113. 'project:read',
  114. 'project:admin',
  115. ];
  116. render(
  117. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  118. );
  119. const enableSpendAllocations = await screen.findByRole('button', {
  120. name: 'Get started',
  121. });
  122. expect(mockGet).toHaveBeenCalledTimes(1);
  123. MockApiClient.clearMockResponses();
  124. const toggleRequestMock = MockApiClient.addMockResponse({
  125. url: `/organizations/${organization.slug}/spend-allocations/toggle/`,
  126. method: 'POST',
  127. });
  128. const createAllocationsRequestMock = MockApiClient.addMockResponse({
  129. url: `/organizations/${organization.slug}/spend-allocations/index/`,
  130. method: 'POST',
  131. });
  132. const mockGet_success = MockApiClient.addMockResponse({
  133. url: `/organizations/${organization.slug}/spend-allocations/`,
  134. method: 'GET',
  135. body: [],
  136. status: 200,
  137. statusCode: 200,
  138. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  139. });
  140. await userEvent.click(enableSpendAllocations);
  141. expect(toggleRequestMock).toHaveBeenCalled();
  142. expect(createAllocationsRequestMock).toHaveBeenCalled();
  143. expect(mockGet_success).toHaveBeenCalledTimes(2);
  144. });
  145. });
  146. describe('enabled Spend Allocations page', () => {
  147. let organization: any, subscription: any, dateTs: any;
  148. beforeEach(() => {
  149. organization = initializeOrg({
  150. organization: {
  151. features: ['spend-allocations'],
  152. },
  153. }).organization;
  154. organization.access = [
  155. 'org:read',
  156. 'org:write',
  157. 'org:admin',
  158. 'org:billing',
  159. 'project:read',
  160. 'project:admin',
  161. ];
  162. subscription = SubscriptionFixture({
  163. organization,
  164. plan: 'am1_f',
  165. planTier: 'am1',
  166. });
  167. MockApiClient.clearMockResponses();
  168. dateTs = Math.max(
  169. new Date().getTime() / 1000,
  170. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  171. );
  172. MockApiClient.addMockResponse({
  173. url: `/organizations/${organization.slug}/spend-allocations/`,
  174. method: 'GET',
  175. body: mockSpendAllocations,
  176. status: 200,
  177. statusCode: 200,
  178. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  179. });
  180. });
  181. it('Does not render with insufficient access', async () => {
  182. organization.access = ['org:read'];
  183. render(
  184. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  185. );
  186. // Waiting a tick for requests to finish
  187. await act(tick);
  188. expect(screen.queryByTestId('spend-allocation-form')).not.toBeInTheDocument();
  189. expect(screen.queryByTestId('subhead-actions')).not.toBeInTheDocument();
  190. });
  191. it('renders allocations table', async () => {
  192. render(
  193. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  194. );
  195. await waitFor(() => screen.findByTestId('allocations-table'));
  196. });
  197. it('renders billing metric select dropdown', async () => {
  198. render(
  199. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  200. );
  201. expect(
  202. await screen.findByRole('button', {name: 'Category Errors'})
  203. ).toBeInTheDocument();
  204. });
  205. it('properly filters allocations by select dropdown', async () => {
  206. render(
  207. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  208. );
  209. const dropdown = await screen.findByRole('button', {name: 'Category Errors'});
  210. await selectEvent.select(dropdown, 'Transactions');
  211. expect(
  212. await screen.findByRole('button', {name: 'Category Transactions'})
  213. ).toBeInTheDocument();
  214. await screen.findByText('Un-Allocated Transactions Pool');
  215. await screen.findAllByTestId('allocation-row');
  216. const tableRows = screen.getAllByTestId('allocation-row');
  217. expect(tableRows).toHaveLength(1); // org allocations are no longer included in table rows
  218. await selectEvent.select(dropdown, 'Attachments');
  219. expect(
  220. await screen.findByRole('button', {name: 'Category Attachments'})
  221. ).toBeInTheDocument();
  222. await screen.findByText('Un-Allocated Attachments Pool');
  223. expect(screen.getByTestId('no-allocations')).toBeInTheDocument();
  224. await selectEvent.openMenu(dropdown);
  225. // assert dropdown options are properly rendered
  226. // This is hacky. the CompactSelect component sets the option value as the test-id
  227. expect(screen.getByTestId('errors')).toBeInTheDocument();
  228. expect(screen.getByTestId('transactions')).toBeInTheDocument();
  229. expect(screen.getByTestId('attachments')).toBeInTheDocument();
  230. });
  231. it('only renders allocation-supported categories that are on the subscription', async () => {
  232. const am3Sub = SubscriptionFixture({
  233. organization,
  234. plan: 'am3_f',
  235. planTier: 'am3',
  236. });
  237. render(<SpendAllocationsRoot organization={organization} subscription={am3Sub} />);
  238. const dropdown = await screen.findByRole('button', {name: 'Category Errors'});
  239. await selectEvent.openMenu(dropdown);
  240. expect(screen.getByTestId('errors')).toBeInTheDocument();
  241. expect(screen.queryByTestId('transactions')).not.toBeInTheDocument();
  242. expect(screen.getByTestId('attachments')).toBeInTheDocument();
  243. });
  244. // NOTE: Period navigation has been removed for now
  245. // eslint-disable-next-line jest/no-disabled-tests
  246. it.skip('refetches allocations on view period change', async () => {
  247. render(
  248. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  249. );
  250. await screen.findAllByTestId('allocation-row');
  251. const tableRows = screen.getAllByTestId('allocation-row');
  252. expect(tableRows).toHaveLength(2); // default metric is error with 2 project mocks
  253. const date = new Date(dateTs * 1000);
  254. date.setMonth(date.getMonth() + 1);
  255. const start = new Date(subscription.onDemandPeriodEnd + 'T00:00:00.000');
  256. start.setDate(start.getDate() + 1);
  257. const nextTs = Math.max(date.getTime() / 1000, new Date(start).getTime() / 1000);
  258. const mockGet_success = MockApiClient.addMockResponse({
  259. url: `/organizations/${organization.slug}/spend-allocations/`,
  260. method: 'GET',
  261. body: mockRootAllocations,
  262. status: 200,
  263. statusCode: 200,
  264. match: [
  265. MockApiClient.matchQuery({
  266. timestamp: nextTs,
  267. }),
  268. ],
  269. });
  270. await userEvent.click(screen.getByTestId('nextPeriod'));
  271. expect(await screen.findByTestId('no-allocations')).toBeInTheDocument();
  272. expect(mockGet_success).toHaveBeenCalledTimes(1);
  273. });
  274. it('deletes allocations on disable', async () => {
  275. render(
  276. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  277. );
  278. await waitFor(() => screen.findByTestId('allocations-table'));
  279. expect(
  280. screen.queryByRole('button', {
  281. name: 'Create Organization-Level Allocation',
  282. })
  283. ).not.toBeInTheDocument();
  284. MockApiClient.clearMockResponses();
  285. const mockDelete = MockApiClient.addMockResponse({
  286. url: `/organizations/${organization.slug}/spend-allocations/index/`,
  287. method: 'DELETE',
  288. });
  289. const mockGet = MockApiClient.addMockResponse({
  290. url: `/organizations/${organization.slug}/spend-allocations/`,
  291. method: 'GET',
  292. body: [],
  293. status: 200,
  294. statusCode: 200,
  295. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  296. });
  297. await userEvent.click(
  298. screen.getByRole('button', {
  299. name: 'Disable Spend Allocations',
  300. })
  301. );
  302. renderGlobalModal();
  303. await userEvent.click(
  304. screen.getByRole('button', {
  305. name: 'Confirm',
  306. })
  307. );
  308. expect(mockDelete).toHaveBeenCalledTimes(1);
  309. expect(mockGet).toHaveBeenCalledTimes(2);
  310. });
  311. });
  312. describe('enabled Spend Allocations page without root', () => {
  313. let organization: any, subscription: any, dateTs: any, mockGet: any;
  314. beforeEach(() => {
  315. organization = initializeOrg({
  316. organization: {
  317. features: ['spend-allocations'],
  318. },
  319. }).organization;
  320. organization.access = [
  321. 'org:read',
  322. 'org:write',
  323. 'org:admin',
  324. 'org:billing',
  325. 'project:read',
  326. 'project:admin',
  327. ];
  328. subscription = SubscriptionFixture({
  329. organization,
  330. plan: 'am1_f',
  331. planTier: 'am1',
  332. });
  333. MockApiClient.clearMockResponses();
  334. dateTs = Math.max(
  335. new Date().getTime() / 1000,
  336. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  337. );
  338. mockGet = MockApiClient.addMockResponse({
  339. url: `/organizations/${organization.slug}/spend-allocations/`,
  340. method: 'GET',
  341. body: [],
  342. status: 200,
  343. statusCode: 200,
  344. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  345. });
  346. });
  347. it('renders missing root card', async () => {
  348. render(
  349. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  350. );
  351. expect(mockGet).toHaveBeenCalledTimes(1);
  352. await screen.findByTestId('missing-root');
  353. });
  354. it('creates root allocation for billing metric', async () => {
  355. render(
  356. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  357. );
  358. await screen.findByRole('button', {
  359. name: 'Create Organization-Level Allocation',
  360. });
  361. expect(mockGet).toHaveBeenCalledTimes(2);
  362. const enableSpendAllocation = screen.getByRole('button', {
  363. name: 'Create Organization-Level Allocation',
  364. });
  365. MockApiClient.clearMockResponses();
  366. const requestMock = MockApiClient.addMockResponse({
  367. url: `/organizations/${organization.slug}/spend-allocations/`,
  368. method: 'POST',
  369. });
  370. const mockGet_success = MockApiClient.addMockResponse({
  371. url: `/organizations/${organization.slug}/spend-allocations/`,
  372. method: 'GET',
  373. body: mockRootAllocations,
  374. status: 200,
  375. statusCode: 200,
  376. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  377. });
  378. await userEvent.click(enableSpendAllocation);
  379. expect(requestMock).toHaveBeenCalled();
  380. expect(mockGet_success).toHaveBeenCalledTimes(2);
  381. });
  382. });
  383. describe('POST Create spend allocation', () => {
  384. let organization: any, subscription: any, projects: any, mockPost: any, dateTs: number;
  385. beforeEach(() => {
  386. projects = [
  387. ProjectFixture({
  388. id: String(mockSpendAllocations[3]!.targetId),
  389. slug: mockSpendAllocations[3]!.targetSlug,
  390. }),
  391. ProjectFixture({
  392. id: String(mockSpendAllocations[4]!.targetId),
  393. slug: mockSpendAllocations[4]!.targetSlug,
  394. }),
  395. ProjectFixture({
  396. // transaction allocation
  397. id: String(mockSpendAllocations[5]!.targetId),
  398. slug: mockSpendAllocations[5]!.targetSlug,
  399. }),
  400. ];
  401. organization = initializeOrg({
  402. organization: {
  403. features: ['spend-allocations'],
  404. },
  405. }).organization;
  406. subscription = SubscriptionFixture({
  407. organization,
  408. plan: 'am1_f',
  409. planTier: 'am1',
  410. });
  411. MockApiClient.clearMockResponses();
  412. dateTs = Math.max(
  413. new Date().getTime() / 1000,
  414. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  415. );
  416. MockApiClient.addMockResponse({
  417. url: `/organizations/${organization.slug}/spend-allocations/`,
  418. method: 'GET',
  419. body: mockSpendAllocations,
  420. status: 200,
  421. statusCode: 200,
  422. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  423. });
  424. mockPost = MockApiClient.addMockResponse({
  425. url: `/organizations/${organization.slug}/spend-allocations/`,
  426. method: 'POST',
  427. status: 200,
  428. statusCode: 200,
  429. });
  430. ProjectsStore.loadInitialData(projects);
  431. });
  432. it('opens and closes form', async () => {
  433. render(
  434. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  435. );
  436. expect(
  437. await screen.findByRole('button', {name: 'New Allocation'})
  438. ).toBeInTheDocument();
  439. await userEvent.click(screen.getByText('New Allocation'));
  440. const {waitForModalToHide} = renderGlobalModal();
  441. expect(await screen.findByRole('button', {name: 'Cancel'})).toBeInTheDocument();
  442. expect(screen.getByTestId('spend-allocation-form')).toBeInTheDocument();
  443. await userEvent.click(screen.getByText('Cancel'));
  444. await waitForModalToHide();
  445. expect(
  446. await screen.findByRole('button', {name: 'New Allocation'})
  447. ).toBeInTheDocument();
  448. expect(screen.queryByTestId('spend-allocation-form')).not.toBeInTheDocument();
  449. });
  450. it('filters target project list', async () => {
  451. // TODO: figure out how to write tests for SelectField component.
  452. });
  453. it('prevents submit on incomplete form', async () => {
  454. render(
  455. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  456. );
  457. expect(
  458. await screen.findByRole('button', {name: 'New Allocation'})
  459. ).toBeInTheDocument();
  460. await userEvent.click(screen.getByText('New Allocation'));
  461. renderGlobalModal();
  462. await userEvent.click(screen.getByText('Submit'));
  463. expect(mockPost.mock.calls).toHaveLength(0);
  464. });
  465. });
  466. describe('Disable Submit button in Spend Allocation', () => {
  467. let organization: any, subscription: any, projects: any, dateTs: number;
  468. beforeEach(() => {
  469. projects = [
  470. ProjectFixture({
  471. id: String(mockSpendAllocations[3]!.targetId),
  472. slug: mockSpendAllocations[3]!.targetSlug,
  473. }),
  474. ProjectFixture({
  475. id: String(mockSpendAllocations[4]!.targetId),
  476. slug: mockSpendAllocations[4]!.targetSlug,
  477. }),
  478. ProjectFixture({
  479. // transaction allocation
  480. id: String(mockSpendAllocations[5]!.targetId),
  481. slug: mockSpendAllocations[5]!.targetSlug,
  482. }),
  483. ];
  484. organization = initializeOrg({
  485. organization: {
  486. features: ['spend-allocations'],
  487. },
  488. }).organization;
  489. subscription = SubscriptionFixture({
  490. organization,
  491. plan: 'am1_f',
  492. planTier: 'am1',
  493. });
  494. MockApiClient.clearMockResponses();
  495. dateTs = Math.max(
  496. new Date().getTime() / 1000,
  497. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  498. );
  499. MockApiClient.addMockResponse({
  500. url: `/organizations/${organization.slug}/spend-allocations/`,
  501. method: 'GET',
  502. body: [],
  503. status: 200,
  504. statusCode: 200,
  505. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  506. });
  507. ProjectsStore.loadInitialData(projects);
  508. });
  509. it('prevents submit with no root allocations', async () => {
  510. render(
  511. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  512. );
  513. expect(
  514. await screen.findByRole('button', {name: 'New Allocation'})
  515. ).toBeInTheDocument();
  516. await userEvent.click(screen.getByText('New Allocation'));
  517. renderGlobalModal();
  518. await userEvent.click(screen.getByText('Select a project to continue'));
  519. expect(screen.getByText(projects[0].slug)).toBeInTheDocument();
  520. await userEvent.click(screen.getByText(projects[0].slug));
  521. expect(screen.getByTestId('spend-allocation-submit')).toBeDisabled();
  522. });
  523. });
  524. describe('DELETE spend allocation', () => {
  525. let organization: any, subscription: any, mockDelete: any, mockGet: any, dateTs: number;
  526. beforeEach(() => {
  527. organization = initializeOrg({
  528. organization: {
  529. features: ['spend-allocations'],
  530. },
  531. }).organization;
  532. subscription = SubscriptionFixture({
  533. organization,
  534. plan: 'am1_f',
  535. planTier: 'am1',
  536. });
  537. MockApiClient.clearMockResponses();
  538. dateTs = Math.max(
  539. new Date().getTime() / 1000,
  540. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  541. );
  542. mockGet = MockApiClient.addMockResponse({
  543. url: `/organizations/${organization.slug}/spend-allocations/`,
  544. method: 'GET',
  545. body: mockSpendAllocations,
  546. status: 200,
  547. statusCode: 200,
  548. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  549. });
  550. mockDelete = MockApiClient.addMockResponse({
  551. url: `/organizations/${organization.slug}/spend-allocations/`,
  552. method: 'DELETE',
  553. status: 200,
  554. statusCode: 200,
  555. match: [
  556. MockApiClient.matchQuery({
  557. billing_metric: 'error',
  558. target_id: 1,
  559. target_type: 'Project',
  560. }),
  561. ],
  562. });
  563. });
  564. it('renders delete button for project allocations', async () => {
  565. render(
  566. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  567. );
  568. await screen.findAllByTestId('allocation-row');
  569. const tableRows = screen.getAllByTestId('allocation-row');
  570. expect(tableRows).toHaveLength(2);
  571. expect(within(tableRows[0]!).getByTestId('delete')).toBeInTheDocument();
  572. expect(within(tableRows[1]!).getByTestId('delete')).toBeInTheDocument();
  573. });
  574. it('fires delete request on click', async () => {
  575. render(
  576. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  577. );
  578. expect(mockGet.mock.calls).toHaveLength(1);
  579. await screen.findAllByTestId('allocation-row');
  580. const tableRows = screen.getAllByTestId('allocation-row');
  581. await userEvent.click(within(tableRows[0]!).getByTestId('delete'));
  582. expect(mockDelete.mock.calls).toHaveLength(1);
  583. // Assert that it refetches allocations on success
  584. expect(mockGet.mock.calls).toHaveLength(4);
  585. });
  586. });
  587. describe('PUT edit spend allocation', () => {
  588. let organization: any, subscription: any, projects: any, mockPut: any, dateTs: number;
  589. beforeEach(() => {
  590. projects = [
  591. ProjectFixture({
  592. id: String(mockSpendAllocations[2]!.targetId),
  593. slug: mockSpendAllocations[2]!.targetSlug,
  594. }),
  595. ProjectFixture({
  596. id: String(mockSpendAllocations[3]!.targetId),
  597. slug: mockSpendAllocations[3]!.targetSlug,
  598. }),
  599. ProjectFixture({
  600. // transaction allocation
  601. id: String(mockSpendAllocations[4]!.targetId),
  602. slug: mockSpendAllocations[4]!.targetSlug,
  603. }),
  604. ];
  605. organization = initializeOrg({
  606. organization: {
  607. features: ['spend-allocations'],
  608. },
  609. }).organization;
  610. subscription = SubscriptionFixture({
  611. organization,
  612. plan: 'am1_f',
  613. planTier: 'am1',
  614. });
  615. MockApiClient.clearMockResponses();
  616. dateTs = Math.max(
  617. new Date().getTime() / 1000,
  618. new Date(subscription.onDemandPeriodStart + 'T00:00:00.000').getTime() / 1000
  619. );
  620. MockApiClient.addMockResponse({
  621. url: `/organizations/${organization.slug}/spend-allocations/`,
  622. method: 'GET',
  623. body: mockSpendAllocations,
  624. status: 200,
  625. statusCode: 200,
  626. match: [MockApiClient.matchQuery({timestamp: dateTs})],
  627. });
  628. mockPut = MockApiClient.addMockResponse({
  629. url: `/organizations/${organization.slug}/spend-allocations/`,
  630. method: 'PUT',
  631. status: 200,
  632. statusCode: 200,
  633. });
  634. ProjectsStore.loadInitialData(projects);
  635. });
  636. it('opens, initializes form on edit, and submits PUT', async () => {
  637. render(
  638. <SpendAllocationsRoot organization={organization} subscription={subscription} />
  639. );
  640. await screen.findAllByTestId('allocation-row');
  641. const tableRows = screen.getAllByTestId('allocation-row');
  642. expect(tableRows).toHaveLength(2);
  643. expect(within(tableRows[0]!).getByTestId('edit')).toBeInTheDocument();
  644. // Should be editing the first 'error' allocation (mockSpendAllocations[2])
  645. await userEvent.click(within(tableRows[0]!).getByTestId('edit'));
  646. renderGlobalModal();
  647. expect(await screen.findByRole('button', {name: 'Cancel'})).toBeInTheDocument();
  648. expect(screen.getByTestId('spend-allocation-form')).toBeInTheDocument();
  649. // Mock currently only includes a single project NOT allocated for errors
  650. expect(screen.queryAllByTestId('badge-display-name')).toHaveLength(1);
  651. expect(screen.getByTestId('allocation-input')).toHaveValue(
  652. mockSpendAllocations[2]!.reservedQuantity
  653. );
  654. expect(screen.getByTestId('toggle-spend')).toBeInTheDocument();
  655. await userEvent.click(screen.getByTestId('toggle-spend'));
  656. expect(screen.getByTestId('allocation-input')).toHaveValue(
  657. (mockSpendAllocations[2]!.reservedQuantity! *
  658. mockSpendAllocations[2]!.costPerItem!) /
  659. 100
  660. );
  661. await userEvent.click(screen.getByText('Save Changes'));
  662. expect(mockPut.mock.calls).toHaveLength(1);
  663. });
  664. });