diff --git a/frontend/src/__tests__/plans-column-filters.test.ts b/frontend/src/__tests__/plans-column-filters.test.ts index e385833cd..6e5dcbfd4 100644 --- a/frontend/src/__tests__/plans-column-filters.test.ts +++ b/frontend/src/__tests__/plans-column-filters.test.ts @@ -141,10 +141,54 @@ describe('Planned Purchases column filters (issue #166 follow-up)', () => { ); const cols = Array.from(buttons).map((b) => b.dataset['column']); expect(cols.sort()).toEqual( - ['count', 'estimated_savings', 'payment', 'provider', 'resource_type', 'service', 'status', 'term', 'upfront_cost'].sort(), + ['count', 'estimated_savings', 'payment', 'plan_name', 'provider', 'resource_type', 'scheduled_date', 'service', 'status', 'term', 'upfront_cost'].sort(), ); }); + test('plan name and scheduled date filter narrow rows (#1680)', async () => { + await loadPlans(); + const uncheck = (column: string, value: string): void => { + document.querySelector( + `#planned-purchases-list th .column-filter-btn[data-column="${column}"]`, + )?.click(); + const cb = Array.from(document.querySelectorAll('.column-filter-popover input[type="checkbox"]')) + .find((i) => i.dataset['value'] === value); + expect(cb).toBeDefined(); + cb!.checked = false; + cb!.dispatchEvent(new Event('change')); + }; + uncheck('plan_name', 'Plan B'); + expect(countRows()).toBe(2); + uncheck('scheduled_date', '2026-06-02'); + expect(countRows()).toBe(1); + }); + + test('scheduled date options are one per displayed day', async () => { + (api.getPlannedPurchases as jest.Mock).mockResolvedValue({ purchases: [ + { ...seedPurchases[0], id: 'd1', scheduled_date: '2026-06-01T10:00:00' }, + { ...seedPurchases[1], id: 'd2', scheduled_date: '2026-06-01T15:00:00' }, + ] }); + await loadPlans(); + document.querySelector( + '#planned-purchases-list th .column-filter-btn[data-column="scheduled_date"]', + )?.click(); + expect(document.querySelectorAll('.column-filter-popover .column-filter-item')).toHaveLength(1); + }); + + test('scheduled date options are in chronological order, labelled by date', async () => { + const dates = ['2026-12-01', '2026-01-10', '2026-01-02', '2026-06-02']; + (api.getPlannedPurchases as jest.Mock).mockResolvedValue({ purchases: dates.map((d, i) => ( + { ...seedPurchases[0], id: `o${i}`, scheduled_date: d } + )) }); + await loadPlans(); + document.querySelector( + '#planned-purchases-list th .column-filter-btn[data-column="scheduled_date"]', + )?.click(); + const labels = Array.from(document.querySelectorAll('.column-filter-popover .column-filter-item')) + .map((l) => l.textContent?.trim()); + expect(labels).toEqual(['Jan 2, 2026', 'Jan 10, 2026', 'Jun 2, 2026', 'Dec 1, 2026']); + }); + test('clicking a trigger opens a popover detached to document.body', async () => { await loadPlans(); const providerBtn = document.querySelector( diff --git a/frontend/src/__tests__/plans-ramp-label.test.ts b/frontend/src/__tests__/plans-ramp-label.test.ts index e76fe2136..946a20d10 100644 --- a/frontend/src/__tests__/plans-ramp-label.test.ts +++ b/frontend/src/__tests__/plans-ramp-label.test.ts @@ -180,3 +180,19 @@ describe('custom ramp inputs when Edit is the first plan modal (#1536)', () => { expect(intervalInput().disabled).toBe(true); }); }); + +describe('ramp step wording says percent of target (#1680)', () => { + beforeEach(() => { document.body.innerHTML = loadRealIndexBody(); }); + + test('custom step field label names the target', () => { + const label = document.getElementById('ramp-step-percent')?.closest('label')?.textContent ?? ''; + expect(label).toContain('% of target'); + }); + + test('preset descriptions say each step is a share of the target', () => { + const desc = (value: string): string => + document.querySelector(`input[value="${value}"]`)?.closest('label')?.querySelector('.ramp-desc')?.textContent ?? ''; + expect(desc('weekly-25pct')).toContain('25% of target each week'); + expect(desc('monthly-10pct')).toContain('10% of target each month'); + }); +}); diff --git a/frontend/src/__tests__/plans.test.ts b/frontend/src/__tests__/plans.test.ts index 94735b070..07bc09b5e 100644 --- a/frontend/src/__tests__/plans.test.ts +++ b/frontend/src/__tests__/plans.test.ts @@ -816,17 +816,53 @@ describe('Plans Module', () => { await loadPlans(); }); - test('renders the run button disabled with an explanatory title', () => { + test('renders the run button aria-disabled (still focusable) with an explanatory title', () => { const runBtn = document.querySelector('[data-action="run"]') as HTMLButtonElement; - expect(runBtn.disabled).toBe(true); + expect(runBtn.disabled).toBe(false); + expect(runBtn.getAttribute('aria-disabled')).toBe('true'); + expect(runBtn.classList.contains('is-disabled')).toBe(true); expect(runBtn.title).toBe('Run now: this step has no recommendations, so there is nothing to run'); expect(runBtn.getAttribute('aria-label')).toBe('Run now: this step has no recommendations, so there is nothing to run'); expect(runBtn.getAttribute('data-id')).toBe('purchase-bare'); }); + test('placeholder dashes share one muted style in the numeric columns (#1680)', () => { + const cells = Array.from(document.querySelectorAll('tr.planned-purchase-row td.num')); + expect(cells).toHaveLength(3); + cells.forEach(td => { + expect(td.innerHTML.trim()).toBe('\u2014'); + }); + }); + + test('the plan name is not clamped to the small truncate width (#1680)', () => { + const name = document.querySelector('.plan-name') as HTMLElement; + expect(name.classList.contains('cell-truncate--sm')).toBe(false); + expect(name.classList.contains('plan-name-cell')).toBe(true); + expect(name.title).toBe('Azure compute 80% weekly'); + }); + + test('clicking the aria-disabled run button keeps focus and does not re-fetch', async () => { + const runBtn = document.querySelector('[data-action="run"]') as HTMLButtonElement; + runBtn.focus(); + (api.getPlannedPurchases as jest.Mock).mockClear(); + runBtn.click(); + await flushPromises(); + + expect(api.getPlannedPurchases).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(runBtn); + }); + + test('an all-upfront step with $0 upfront shows the placeholder dash', async () => { + (api.getPlannedPurchases as jest.Mock).mockResolvedValue({ purchases: [ + { ...bare, has_recommendations: true, payment: 'all-upfront', upfront_cost: 0, count: 2, estimated_savings: 5 }, + ] }); + await loadPlans(); + const cells = document.querySelectorAll('tr.planned-purchase-row td.num'); + expect(cells[1]!.innerHTML.trim()).toBe('\u2014'); + }); + test('the run action never calls the API or opens a confirm', async () => { const runBtn = document.querySelector('[data-action="run"]') as HTMLButtonElement; - runBtn.disabled = false; runBtn.click(); await flushPromises(); diff --git a/frontend/src/index.html b/frontend/src/index.html index 4c34fcfe7..1e316e598 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1075,12 +1075,12 @@

Ramp-up Schedule