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
47 changes: 47 additions & 0 deletions frontend/src/__tests__/settings-accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import {
loadAccountsForProvider,
loadOverridesPanel,
setupSettingsHandlers
} from '../settings';

Expand Down Expand Up @@ -925,4 +926,50 @@ describe('Account overrides modal', () => {
// Verify it is gone — the only override container is the modal body.
expect(document.querySelector('.account-overrides-panel')).toBeNull();
});

test('Delete-override button + dialog wording match the actual data semantics (issue #114)', async () => {
// The action DELETEs the override row; pre-#114 the dialog said
// "Reset … will be replaced" which implied a stuck-around row with
// new values. Pin the post-fix wording so a future regression doesn't
// silently re-introduce the mismatch.
(api.listAccountServiceOverrides as jest.Mock).mockResolvedValue([
{
account_id: 'acc-1',
provider: 'aws',
service: 'ec2',
term: 1,
payment: 'no-upfront',
coverage: 80,
},
]);
mockConfirmDialog.mockResolvedValue(false); // user cancels — we don't need the API call to fire

const panel = document.createElement('div');
document.body.appendChild(panel);
await loadOverridesPanel('acc-1', panel, 'aws');

// The action button now reads "Delete" (not "Reset").
const buttons = Array.from(panel.querySelectorAll('button'));
const deleteBtn = buttons.find(b => b.textContent === 'Delete');
expect(deleteBtn).toBeDefined();
expect(buttons.find(b => b.textContent === 'Reset')).toBeUndefined();

// Click it and inspect the confirmDialog opts.
deleteBtn!.click();
await Promise.resolve(); // let the click handler's await chain start
expect(mockConfirmDialog).toHaveBeenCalledTimes(1);
const opts = mockConfirmDialog.mock.calls[0]![0] as {
title: string;
body: string;
confirmLabel: string;
};
expect(opts.title).toBe('Delete override?');
expect(opts.confirmLabel).toBe('Delete override');
expect(opts.body).toContain('Delete the aws/ec2 override');
expect(opts.body).toContain('revert to the global default');
expect(opts.body).toContain('removed');
// Pre-#114 wording must not appear.
expect(opts.body).not.toContain('replaced');
expect(opts.body).not.toContain('Reset');
});
});
2 changes: 1 addition & 1 deletion frontend/src/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as api from './api';
import * as state from './state';
import { formatCurrency, formatTerm, escapeHtml, formatRelativeTime } from './utils';
import { formatCurrency, formatTerm, escapeHtml } from './utils';
import { renderFreshness } from './freshness';
import { getRecommendationDetail, type RecommendationDetail } from './api/recommendations';
import { showToast } from './toast';
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ function buildPaymentOverrideSelect(
// payment value but can't accidentally pick a no-op state.
if (initial !== '') {
inheritOpt.disabled = true;
inheritOpt.title = 'Use Reset to clear all override fields including payment';
inheritOpt.title = 'Use Delete to remove the override entirely (clears all fields including payment)';
}

select.addEventListener('change', () => {
Expand Down Expand Up @@ -640,7 +640,7 @@ function closeAccountOverridesModal(): void {
closeOverrideModal();
}

async function loadOverridesPanel(accountId: string, panel: HTMLElement, provider: AccountProvider): Promise<void> {
export async function loadOverridesPanel(accountId: string, panel: HTMLElement, provider: AccountProvider): Promise<void> {
panel.textContent = 'Loading\u2026';
try {
const overrides = await api.listAccountServiceOverrides(accountId);
Expand Down Expand Up @@ -721,20 +721,25 @@ async function loadOverridesPanel(accountId: string, panel: HTMLElement, provide
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'btn btn-small btn-danger';
resetBtn.textContent = 'Reset';
// Button + dialog wording aligned with the actual data semantics
// (DELETE on account_service_overrides) per #114. The previous
// "Reset … will be replaced" copy implied the row stuck around
// with new values; in fact the row goes away entirely and the
// engine reads the global default as a side effect of its absence.
resetBtn.textContent = 'Delete';
resetBtn.addEventListener('click', async () => {
const ok = await confirmDialog({
title: 'Reset override?',
body: `Reset ${o.provider}/${o.service} override to the global default? Any per-service values you set will be replaced.`,
confirmLabel: 'Reset override',
title: 'Delete override?',
body: `Delete the ${o.provider}/${o.service} override? This account's recommendations will revert to the global default. The override row and any per-service values you set will be removed.`,
confirmLabel: 'Delete override',
destructive: true,
});
if (!ok) return;
try {
await api.deleteAccountServiceOverride(accountId, o.provider, o.service);
await loadOverridesPanel(accountId, panel, provider);
} catch (err) {
showToast({ message: `Failed to reset override: ${(err as Error).message}`, kind: 'error' });
showToast({ message: `Failed to delete override: ${(err as Error).message}`, kind: 'error' });
}
});
actionTd.appendChild(resetBtn);
Expand Down