diff --git a/frontend/src/__tests__/inventory.test.ts b/frontend/src/__tests__/inventory.test.ts index 4cf38e2f0..532b69f25 100644 --- a/frontend/src/__tests__/inventory.test.ts +++ b/frontend/src/__tests__/inventory.test.ts @@ -435,7 +435,10 @@ describe('loadCoverageBreakdown — fetch + render flow', () => { expect(rows[1]!.textContent).toContain('100.0%'); }); - test('renders "No usage detected" for providers with null services', async () => { + test('renders "No usage detected" for providers with null services when no chip is active', async () => { + // No chip active: genuinely no usage -> original message. + (state.getCurrentProvider as jest.Mock).mockReturnValue(''); + (state.getCurrentAccountIDs as jest.Mock).mockReturnValue([]); (api.getCoverageBreakdown as jest.Mock).mockResolvedValue({ providers: [ makeProviderSection('aws', null, null), @@ -450,6 +453,47 @@ describe('loadCoverageBreakdown — fetch + render flow', () => { const empties = container.querySelectorAll('.empty'); expect(empties.length).toBe(3); expect(empties[0]!.textContent).toContain('AWS'); + expect(empties[0]!.textContent).toMatch(/no usage detected/i); + }); + + test('renders filter-aware empty state when provider chip is active and services are empty', async () => { + // Provider chip set -> chip filtered all rows; message must not claim "No usage detected". + (state.getCurrentProvider as jest.Mock).mockReturnValue('gcp'); + (state.getCurrentAccountIDs as jest.Mock).mockReturnValue([]); + (api.getCoverageBreakdown as jest.Mock).mockResolvedValue({ + providers: [ + makeProviderSection('gcp', null, null), + ], + }); + + await loadCoverageBreakdown(); + + const container = document.getElementById('coverage-providers')!; + const empty = container.querySelector('.empty'); + expect(empty).not.toBeNull(); + expect(empty!.textContent).toContain('GCP'); + expect(empty!.textContent).toMatch(/selected account\/filter/i); + expect(empty!.textContent).not.toMatch(/no usage detected/i); + }); + + test('renders filter-aware empty state when account chip is active and services are empty', async () => { + // Account chip set (single account) -> message must reference selected filter. + (state.getCurrentProvider as jest.Mock).mockReturnValue(''); + (state.getCurrentAccountIDs as jest.Mock).mockReturnValue(['acct-99']); + (api.getCoverageBreakdown as jest.Mock).mockResolvedValue({ + providers: [ + makeProviderSection('aws', null, null), + ], + }); + + await loadCoverageBreakdown(); + + const container = document.getElementById('coverage-providers')!; + const empty = container.querySelector('.empty'); + expect(empty).not.toBeNull(); + expect(empty!.textContent).toContain('AWS'); + expect(empty!.textContent).toMatch(/selected account\/filter/i); + expect(empty!.textContent).not.toMatch(/no usage detected/i); }); test('renders N/A for null coverage_pct (no usage signal on that service)', async () => { diff --git a/frontend/src/inventory.ts b/frontend/src/inventory.ts index 10f543a5a..6abe55371 100644 --- a/frontend/src/inventory.ts +++ b/frontend/src/inventory.ts @@ -178,6 +178,26 @@ function buildActiveCommitmentsEmptyMessage(provider?: string, accountID?: strin return 'No active commitments found across your registered accounts.'; } +/** + * Build a context-aware empty-state message for a per-provider Coverage card. + * When chip filters are active the message names the scope so the user knows + * the result is filtered rather than genuinely absent usage. + * + * @param providerLabel - Display name of the provider (e.g. "AWS"). + * @param activeProvider - The provider chip value, if one is set. + * @param activeAccountID - The account chip value, if exactly one is selected. + */ +function buildCoverageEmptyMessage( + providerLabel: string, + activeProvider?: string, + activeAccountID?: string, +): string { + if (activeProvider || activeAccountID) { + return `No ${providerLabel} usage for the selected account/filter.`; + } + return `No usage detected for ${providerLabel}.`; +} + /** * Render the per-commitment table into `container`. Empty list yields * an inline `.empty` paragraph instead of an empty table so the user @@ -302,7 +322,7 @@ export async function loadCoverageBreakdown(): Promise { try { const data = await api.getCoverageBreakdown({ provider: provider || undefined, accountID }); - renderCoverageBreakdown(container, data.providers); + renderCoverageBreakdown(container, data.providers, provider || undefined, accountID); } catch (error) { teardownSkeleton(container); const err = error as Error; @@ -326,7 +346,12 @@ function wireCoverageRefreshButton(): void { * textContent -- no innerHTML -- so no escaping helper is needed (XSS posture * matches the active-commitments section per issue #340). */ -function renderCoverageBreakdown(container: HTMLElement, providers: ProviderCoverageSection[]): void { +function renderCoverageBreakdown( + container: HTMLElement, + providers: ProviderCoverageSection[], + activeProvider?: string, + activeAccountID?: string, +): void { clearChildren(container); if (!providers || providers.length === 0) { @@ -335,11 +360,15 @@ function renderCoverageBreakdown(container: HTMLElement, providers: ProviderCove } for (const section of providers) { - container.appendChild(buildProviderSection(section)); + container.appendChild(buildProviderSection(section, activeProvider, activeAccountID)); } } -function buildProviderSection(section: ProviderCoverageSection): HTMLElement { +function buildProviderSection( + section: ProviderCoverageSection, + activeProvider?: string, + activeAccountID?: string, +): HTMLElement { const card = document.createElement('section'); card.className = 'card coverage-provider-card'; @@ -347,8 +376,10 @@ function buildProviderSection(section: ProviderCoverageSection): HTMLElement { const header = document.createElement('div'); header.className = 'section-header'; + const providerLabel = PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider.toUpperCase(); + const title = document.createElement('h3'); - title.textContent = PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider.toUpperCase(); + title.textContent = providerLabel; header.appendChild(title); if (section.overall_coverage_pct !== null && section.overall_coverage_pct !== undefined) { @@ -363,7 +394,7 @@ function buildProviderSection(section: ProviderCoverageSection): HTMLElement { if (!section.services || section.services.length === 0) { const empty = document.createElement('p'); empty.className = 'empty'; - empty.textContent = `No usage detected for ${PROVIDER_DISPLAY_NAMES[section.provider] ?? section.provider}.`; + empty.textContent = buildCoverageEmptyMessage(providerLabel, activeProvider, activeAccountID); card.appendChild(empty); return card; }