From 082f53d5a86c4fbce98320912b84b4fcf6bdd7c2 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 22:08:46 +0000 Subject: [PATCH] fix(frontend): make the Home row Cancel an outlined destructive button PR #765 defined the button system (quiet outlined default, .btn-danger, .btn-link, .btn-warning, .secondary), which fixed most of this issue. The Upcoming Scheduled Purchases rows on Home still used the filled `danger` class for Cancel, so a solid red block remained the loudest element on every row, ahead of View Details. Switch it to .btn-danger: red border and text so it still reads as destructive, filled only on hover. The detail dialog's "Cancel Purchase" keeps the filled style because there it is the dialog's main action. A jest test pins the class and a Playwright check verifies the painted background, text and border colors against the real stylesheet. Refs #695 Co-Authored-By: Claude Opus 5.5 --- frontend/src/__tests__/dashboard.test.ts | 16 ++++++++++++++++ frontend/src/dashboard.ts | 5 ++++- .../tests-e2e/upcoming-purchases-layout.spec.ts | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) 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); +});