From 9d7ad04f59275395cfa5ec290677fb0ff4638121 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 1 Oct 2026 20:40:52 +0200 Subject: [PATCH 1/2] fix(frontend): remove unsupported purchase cancellation assurance --- .../__tests__/purchase-modal-submit.test.ts | 31 ++++++++ frontend/src/recommendations.ts | 2 +- .../tests-e2e/execute-now-warning.spec.ts | 75 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 frontend/tests-e2e/execute-now-warning.spec.ts diff --git a/frontend/src/__tests__/purchase-modal-submit.test.ts b/frontend/src/__tests__/purchase-modal-submit.test.ts index 4aa76c95..2564a21c 100644 --- a/frontend/src/__tests__/purchase-modal-submit.test.ts +++ b/frontend/src/__tests__/purchase-modal-submit.test.ts @@ -269,6 +269,37 @@ beforeEach(() => { (state.getRecommendations as jest.Mock).mockReturnValue(buildRows()); }); +describe('Issue #251: Execute Now makes no cancellation promise', () => { + test.each>([ + { provider: 'aws', service: 'ec2', payment: 'all-upfront', upfront_cost: 1200 }, + { provider: 'azure', service: 'compute', payment: 'all-upfront', upfront_cost: 2400 }, + { provider: 'gcp', service: 'gce', payment: 'monthly', upfront_cost: 0 }, + ])('$provider shows the charge and approval bypass only', async (sample) => { + const rec: LocalRecommendation = { ...buildRows()[0]!, ...sample, term: 1 }; + (state.getRecommendations as jest.Mock).mockReturnValue([rec]); + await openPurchaseModal([rec]); + const warning = document.querySelector('.direct-execute-warning')!; + expect(warning).not.toBeNull(); + expect(warning.hidden).toBe(true); + (document.getElementById('execute-mode-direct') as HTMLInputElement).click(); + expect(warning.hidden).toBe(false); + expect(warning.textContent).toBe( + `Warning: This will charge ${formatCurrency(sample.upfront_cost, '$', 2)} upfront immediately. This bypasses the approval step.`, + ); + expect(warning.textContent).not.toMatch(/AWS|cancell|24 hours/); + expect(document.getElementById('execute-purchase-btn')?.textContent).toBe('Execute Purchase Now'); + (document.getElementById('execute-mode-approval') as HTMLInputElement).click(); + expect(warning.hidden).toBe(true); + expect(document.getElementById('execute-purchase-btn')?.textContent).toBe('Send for Approval'); + (document.getElementById('execute-mode-direct') as HTMLInputElement).click(); + (document.getElementById('close-purchase-modal-btn') as HTMLButtonElement).click(); + await openPurchaseModal([rec]); + expect((document.getElementById('execute-mode-approval') as HTMLInputElement).checked).toBe(true); + expect(document.querySelector('.direct-execute-warning')!.hidden).toBe(true); + expect(api.executePurchase).not.toHaveBeenCalled(); + }); +}); + // #1903: purchase modal re-prices on Term/Payment change describe('Issue #1903: purchase modal re-prices on Term/Payment change', () => { diff --git a/frontend/src/recommendations.ts b/frontend/src/recommendations.ts index 9424f4eb..ae8dc316 100644 --- a/frontend/src/recommendations.ts +++ b/frontend/src/recommendations.ts @@ -5078,7 +5078,7 @@ function renderDirectExecuteWarning(): void { directWarning.appendChild(icon); const text = document.createTextNode( `This will charge ${formatCurrency(totalUpfront, '$', 2)} upfront immediately. ` + - 'This bypasses the approval step. AWS allows cancellation within 24 hours via the Account & Billing console.', + 'This bypasses the approval step.', ); directWarning.appendChild(text); } diff --git a/frontend/tests-e2e/execute-now-warning.spec.ts b/frontend/tests-e2e/execute-now-warning.spec.ts new file mode 100644 index 00000000..2a0f81c9 --- /dev/null +++ b/frontend/tests-e2e/execute-now-warning.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { mockApi, seedAuth } from './fixtures/recs'; + +for (const sample of [ + { provider: 'aws', service: 'ec2', cloud_account_id: 'acct-001', resource_type: 'm5.large', region: 'us-east-1', payment: 'all-upfront', upfront_cost: 1200 }, + { provider: 'azure', service: 'compute', cloud_account_id: 'acct-100', resource_type: 'Standard_D2_v3', region: 'eastus', payment: 'all-upfront', upfront_cost: 2400 }, + { provider: 'gcp', service: 'gce', cloud_account_id: 'acct-200', resource_type: 'n2-standard-2', region: 'us-central1', payment: 'monthly', upfront_cost: 0 }, +]) { + test(`${sample.provider} Execute Now warns without promising cancellation`, async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); + page.on('response', response => { if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`); }); + const api = await mockApi(page); + await seedAuth(page); + const row = { ...sample, id: `warning-${sample.provider}`, term: 1, count: 1, monthly_cost: sample.upfront_cost ? 0 : 240, savings: 100, on_demand_monthly: 340, effective_savings_pct: 29 }; + await page.route(/\/api\/recommendations(?:\?.*)?$/, route => route.fulfill({ json: { + recommendations: [row], summary: { total_recommendations: 1, total_upfront_cost: row.upfront_cost, potential_monthly_savings: 100, avg_payback_months: 6 }, regions: [row.region], + } })); + await page.route('**/api/accounts/*/service-overrides', route => route.fulfill({ json: [] })); + await page.goto('/opportunities'); + const selected = page.locator(`tr.recommendation-row[data-rec-id="${row.id}"]`); + await expect(selected).toBeVisible(); + await selected.getByRole('checkbox', { name: 'Select recommendation' }).check(); + await page.locator('#bulk-purchase-btn').click(); + await expect(page.locator('#purchase-details')).toContainText(row.resource_type); + const warning = page.locator('#purchase-details .direct-execute-warning'); + await expect(warning).toBeHidden(); + await page.locator('#execute-mode-direct').check(); + await expect(warning).toBeVisible(); + await expect(warning).toHaveText(`Warning: This will charge $${row.upfront_cost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} upfront immediately. This bypasses the approval step.`); + await expect(warning).not.toContainText(/AWS|cancell|24 hours/); + await expect(page.locator('#execute-purchase-btn')).toHaveText('Execute Purchase Now'); + await page.getByRole('checkbox', { name: 'Include row 1' }).uncheck(); + await expect(warning).toHaveText('Warning: This will charge $0.00 upfront immediately. This bypasses the approval step.'); + await expect(page.locator('#execute-purchase-btn')).toBeDisabled(); + await page.getByRole('checkbox', { name: 'Include row 1' }).check(); + await expect(warning).toContainText(`$${row.upfront_cost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} upfront immediately.`); + await expect(page.locator('#execute-purchase-btn')).toBeEnabled(); + if (sample.provider === 'gcp') { + await page.setViewportSize({ width: 390, height: 844 }); + await expect(warning).toBeInViewport(); + const box = await warning.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.x).toBeGreaterThanOrEqual(0); + expect(box!.x + box!.width).toBeLessThanOrEqual(390); + } + await page.locator('#execute-mode-approval').check(); + await expect(warning).toBeHidden(); + await expect(page.locator('#execute-purchase-btn')).toHaveText('Send for Approval'); + await page.locator('#execute-mode-direct').check(); + await page.locator('#close-purchase-modal-btn').click(); + await page.locator('#bulk-purchase-btn').click(); + await expect(page.locator('#execute-mode-approval')).toBeChecked(); + await expect(warning).toBeHidden(); + expect(api.calls.filter(call => call.method === 'POST' && call.url.includes('/purchases/execute'))).toEqual([]); + expect(errors).toEqual([]); + }); +} + +test('mixed providers use fan-out without cancellation reassurance', async ({ page }) => { + const api = await mockApi(page); + await seedAuth(page); + await page.route('**/api/accounts/*/service-overrides', route => route.fulfill({ json: [] })); + await page.goto('/opportunities'); + for (const id of ['r01', 'r12']) { + await page.locator(`tr.recommendation-row[data-rec-id="${id}"]`) + .getByRole('checkbox', { name: 'Select recommendation' }).check(); + } + await page.locator('#bulk-purchase-btn').click(); + await expect(page.locator('#fanout-summary')).toBeVisible(); + await expect(page.locator('#purchase-details')).not.toContainText(/cancellation|24 hours/); + await expect(page.locator('#execute-mode-direct')).toHaveCount(0); + expect(api.calls.filter(call => call.method === 'POST' && call.url.includes('/purchases/execute'))).toEqual([]); +}); From 9d276f5a7269ec0c8c1d4e09da4b91ca2dd9281b Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 5 Oct 2026 01:07:35 +0200 Subject: [PATCH 2/2] fix(frontend): drop the cancellation promise from the execute confirm dialog The direct-execute confirm dialog shown after clicking Execute Purchase Now still told users AWS allows cancellation within 24 hours. Remove the sentence and correct the nearby comment. Add a jest test that opens the modal in direct mode, clicks Execute, and asserts the confirmDialog title and body contain no cancellation promise. Refs #251 --- .../src/__tests__/purchase-modal-submit.test.ts | 14 ++++++++++++++ frontend/src/app.ts | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/purchase-modal-submit.test.ts b/frontend/src/__tests__/purchase-modal-submit.test.ts index 2564a21c..d10cf9f0 100644 --- a/frontend/src/__tests__/purchase-modal-submit.test.ts +++ b/frontend/src/__tests__/purchase-modal-submit.test.ts @@ -298,6 +298,20 @@ describe('Issue #251: Execute Now makes no cancellation promise', () => { expect(document.querySelector('.direct-execute-warning')!.hidden).toBe(true); expect(api.executePurchase).not.toHaveBeenCalled(); }); + + test('direct-execute confirm dialog makes no cancellation promise', async () => { + const rec: LocalRecommendation = { ...buildRows()[0]!, provider: 'aws', service: 'ec2', term: 1 }; + (state.getRecommendations as jest.Mock).mockReturnValue([rec]); + (confirmDialog as jest.Mock).mockClear(); + await openPurchaseModal([rec]); + (document.getElementById('execute-mode-direct') as HTMLInputElement).click(); + (document.getElementById('execute-purchase-btn') as HTMLButtonElement).click(); + await flush(); + expect(confirmDialog).toHaveBeenCalledTimes(1); + const opts = (confirmDialog as jest.Mock).mock.calls[0]![0] as { title: string; body: string }; + expect(opts.body).toMatch(/charge the full upfront amount immediately/); + expect(`${opts.title} ${opts.body}`).not.toMatch(/AWS|cancell|24 hours/i); + }); }); // #1903: purchase modal re-prices on Term/Payment change diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 95df5f37..85400162 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -382,11 +382,11 @@ export async function handleExecutePurchase(): Promise { // Confirmation dialog varies by mode: // - Approval path: low-friction, non-destructive. // - Direct-execute path: red destructive dialog with cost callout and - // cancellation-window reminder (issue #289 acceptance criteria). + // immediate-charge callout; no cancellation promise (issue #251). const ok = isDirect ? await confirmDialog({ title: `Execute ${localRecs.length} purchase${localRecs.length === 1 ? '' : 's'} now?`, - body: 'This will charge the full upfront amount immediately. This bypasses the approval step. AWS allows cancellation within 24 hours via the Account & Billing console.', + body: 'This will charge the full upfront amount immediately. This bypasses the approval step.', confirmLabel: 'Execute Purchase Now', destructive: true, })