Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/src/__tests__/purchase-modal-submit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,51 @@ beforeEach(() => {
(state.getRecommendations as jest.Mock).mockReturnValue(buildRows());
});

describe('Issue #251: Execute Now makes no cancellation promise', () => {
test.each<Pick<LocalRecommendation, 'provider' | 'service' | 'payment' | 'upfront_cost'>>([
{ 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<HTMLElement>('.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<HTMLElement>('.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

describe('Issue #1903: purchase modal re-prices on Term/Payment change', () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,11 @@ export async function handleExecutePurchase(): Promise<void> {
// 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,
})
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
75 changes: 75 additions & 0 deletions frontend/tests-e2e/execute-now-warning.spec.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
Loading