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
30 changes: 29 additions & 1 deletion frontend/src/__tests__/riexchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,12 +1594,40 @@ describe('excluded RIs notice', () => {
await loadReshapeRecommendations();

const notice = document.getElementById('ri-exchange-recommendations-excluded');
expect(notice?.textContent).toContain('2 convertible RIs were left out because AWS returned no term length');
expect(notice?.textContent).toContain('2 convertible RIs were left out because no term');
expect(notice?.textContent).toContain('ri-0');
expect(notice?.textContent).toContain('<img src=x onerror=alert(1)>');
expect(notice?.querySelector('img')).toBeNull();
});

it('shows each excluded RI\'s server reason, grouped by reason', () => {
renderExcludedRIsNotice([
{ ri_id: 'ri-dur', reason: 'non-positive duration' },
{ ri_id: 'ri-price', reason: 'no pricing available' },
]);
const text = document.getElementById('ri-exchange-recommendations-excluded')?.textContent ?? '';
expect(text).toContain('1 convertible RI was left out because non-positive duration: ri-dur');
expect(text).toContain('1 convertible RI was left out because no pricing available: ri-price');
expect(text).not.toContain('no term length');
});

it('falls back to the term-length text when reason is empty or missing', () => {
renderExcludedRIsNotice([
{ ri_id: 'ri-a', reason: '' },
{ ri_id: 'ri-b' } as unknown as { ri_id: string; reason: string },
]);
expect(document.getElementById('ri-exchange-recommendations-excluded')?.textContent).toContain(
'2 convertible RIs were left out because AWS returned no term length: ri-a, ri-b',
);
});

it('renders a reason containing HTML special characters as text', () => {
renderExcludedRIsNotice([{ ri_id: 'ri-x', reason: '<b>bad</b> & "quoted"' }]);
const notice = document.getElementById('ri-exchange-recommendations-excluded');
expect(notice?.textContent).toContain('<b>bad</b> & "quoted"');
expect(notice?.querySelector('b')).toBeNull();
});

it('shows no notice when excluded_ris is empty or absent, and clears a stale one', () => {
renderExcludedRIsNotice([{ ri_id: 'ri-0', reason: 'x' }]);
expect(document.getElementById('ri-exchange-recommendations-excluded')?.textContent).toContain('1 convertible RI was left out');
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/riexchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,18 @@ export function renderExcludedRIsNotice(excluded: ExcludedRI[] | undefined): voi
notice.className = '';
return;
}
const n = excluded.length;
const byReason = new Map<string, string[]>();
for (const e of excluded) {
const reason = e.reason || 'AWS returned no term length';
const ids = byReason.get(reason);
if (ids) ids.push(e.ri_id);
else byReason.set(reason, [e.ri_id]);
}
notice.className = 'freshness-banner warning';
notice.textContent = `${n} convertible RI${n === 1 ? ' was' : 's were'} left out because AWS returned no term length: ${excluded.map((e) => e.ri_id).join(', ')}`;
// textContent keeps server-provided reasons and ids inert.
notice.textContent = [...byReason]
.map(([reason, ids]) => `${ids.length} convertible RI${ids.length === 1 ? ' was' : 's were'} left out because ${reason}: ${ids.join(', ')}`)
.join('; ');
}

/**
Expand Down
Loading