diff --git a/frontend/src/__tests__/dashboard.test.ts b/frontend/src/__tests__/dashboard.test.ts index 9a296bec6..516fa9d1d 100644 --- a/frontend/src/__tests__/dashboard.test.ts +++ b/frontend/src/__tests__/dashboard.test.ts @@ -451,6 +451,22 @@ describe('Dashboard Module', () => { expect(upcomingList?.innerHTML).toContain('data-action="cancel-purchase"'); }); + test('row-level cancel button is the outlined destructive variant, not the filled one', async () => { + (api.getDashboardSummary as jest.Mock).mockResolvedValue({ potential_monthly_savings: 1000, by_service: {} }); + (api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ + purchases: [{ + execution_id: 'exec-1', plan_id: 'plan-1', plan_name: 'Test Plan', provider: 'aws', service: 'ec2', + step_number: 1, total_steps: 4, estimated_savings: 100, scheduled_date: '2024-02-15', + }], + }); + + await loadDashboard(); + + const cancelBtn = document.querySelector('#upcoming-list [data-action="cancel-purchase"]'); + expect(cancelBtn?.classList.contains('btn-danger')).toBe(true); + expect(cancelBtn?.classList.contains('danger')).toBe(false); + }); + test('handles missing canvas element gracefully', async () => { document.body.innerHTML = `
diff --git a/frontend/src/dashboard.ts b/frontend/src/dashboard.ts index 2a8e61716..e0032745b 100644 --- a/frontend/src/dashboard.ts +++ b/frontend/src/dashboard.ts @@ -594,7 +594,10 @@ function renderUpcomingPurchases(purchases: UpcomingPurchase[]): void { const cancelBtn = document.createElement('button'); cancelBtn.dataset['action'] = 'cancel-purchase'; cancelBtn.dataset['id'] = String(p.execution_id); - cancelBtn.className = 'danger'; + // Outlined destructive (issue #695): the filled red `danger` style made + // Cancel the loudest element on every row. It still reads as + // destructive and fills on hover; the confirm dialog guards the action. + cancelBtn.className = 'btn-danger'; cancelBtn.textContent = 'Cancel'; actions.appendChild(cancelBtn); } diff --git a/frontend/tests-e2e/upcoming-purchases-layout.spec.ts b/frontend/tests-e2e/upcoming-purchases-layout.spec.ts index 22cb3f82e..1d6c5b82d 100644 --- a/frontend/tests-e2e/upcoming-purchases-layout.spec.ts +++ b/frontend/tests-e2e/upcoming-purchases-layout.spec.ts @@ -250,3 +250,19 @@ test('the savings label keeps one line without overflowing the card', async ({ p } } }); + +// Issue #695: the row's Cancel was a filled red block, the loudest element on +// Home. It stays visibly destructive (red border and text) but is outlined, +// so the paint check below runs against the real stylesheet, not the class. +test('the row Cancel button is an outlined destructive action, not a filled block', async ({ page }) => { + await openHome(page); + const cancel = page.locator('.upcoming-card [data-action="cancel-purchase"]').first(); + const paint = await cancel.evaluate((el) => { + const cs = getComputedStyle(el); + return { bg: cs.backgroundColor, fg: cs.color, border: cs.borderTopColor }; + }); + const filledRed = 'rgb(217, 48, 37)'; // --cudly-error + expect(paint.bg).not.toBe(filledRed); + expect(paint.fg).not.toBe('rgb(255, 255, 255)'); + expect(paint.border).toBe(filledRed); +});