diff --git a/frontend/src/__tests__/settings.test.ts b/frontend/src/__tests__/settings.test.ts index a5cd806ca..05cd00a4c 100644 --- a/frontend/src/__tests__/settings.test.ts +++ b/frontend/src/__tests__/settings.test.ts @@ -965,6 +965,71 @@ describe('Settings Module', () => { expect(coverageSentFor('savings-plans-compute')).toBe(20); }); + test('a rejected global PUT leaves the coverage dirty and sends no service PUTs', async () => { + await loadWithCoverage50(); + (document.getElementById('setting-default-coverage') as HTMLInputElement).value = '70'; + (api.updateConfig as jest.Mock).mockRejectedValueOnce(new Error('boom')); + + await saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + + expect(api.updateServiceConfig).not.toHaveBeenCalled(); + expect(isUnsavedChanges()).toBe(true); + }); + + test('an SP card coverage whose service PUT failed stays dirty and is resent', async () => { + await loadWithCoverage50(); + (document.getElementById('aws-savings-plans-compute-coverage') as HTMLInputElement).value = '70'; + (api.updateServiceConfig as jest.Mock).mockImplementation(async (provider: string, svc: string) => { + if (provider === 'aws' && svc === 'savings-plans-compute') throw new Error('sp failed'); + }); + + await saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + expect(isUnsavedChanges()).toBe(true); + + (api.updateServiceConfig as jest.Mock).mockClear().mockResolvedValue(undefined); + await saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + + expect(coverageSentFor('savings-plans-compute')).toBe(70); + expect(isUnsavedChanges()).toBe(false); + }); + + test('an SP card coverage typed during its pending PUT is kept and sent by the next save (#576)', async () => { + await loadWithCoverage50(); + const card = document.getElementById('aws-savings-plans-compute-coverage') as HTMLInputElement; + card.value = '60'; + let resolvePut!: () => void; + (api.updateServiceConfig as jest.Mock).mockImplementation((provider: string, svc: string) => + provider === 'aws' && svc === 'savings-plans-compute' + ? new Promise(resolve => { resolvePut = resolve; }) + : Promise.resolve()); + + const firstSave = saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + await new Promise(r => setTimeout(r, 0)); + card.value = '77'; + resolvePut(); + await firstSave; + + expect(coverageSentFor('savings-plans-compute')).toBe(60); + expect(card.value).toBe('77'); + expect(isUnsavedChanges()).toBe(true); + + (api.updateServiceConfig as jest.Mock).mockClear().mockResolvedValue(undefined); + await saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + + expect(coverageSentFor('savings-plans-compute')).toBe(77); + expect(isUnsavedChanges()).toBe(false); + }); + + test('a saved service payment edit leaves nothing unsaved (#576)', async () => { + await loadWithCoverage50(); + (document.getElementById('aws-ec2-payment') as HTMLSelectElement).value = 'partial-upfront'; + expect(isUnsavedChanges()).toBe(true); + + await saveGlobalSettings({ preventDefault: jest.fn() } as unknown as Event); + + expect(isUnsavedChanges()).toBe(false); + }); + test('an empty global coverage field is rejected instead of saving 0', async () => { await loadWithCoverage50(); (document.getElementById('setting-default-coverage') as HTMLInputElement).value = ''; diff --git a/frontend/src/settings.ts b/frontend/src/settings.ts index ba9e6e88d..acc06cfa7 100644 --- a/frontend/src/settings.ts +++ b/frontend/src/settings.ts @@ -3591,9 +3591,7 @@ export async function saveGlobalSettings(e: Event): Promise { // controls. Read from the DOM when the card has the controls; fall back // to the base row value (or the global default) otherwise so RI and // Azure/GCP cards continue to inherit the global settings. - let coverage = defaultCoverageChanged - ? settings.default_coverage - : (base?.coverage ?? settings.default_coverage); + let coverage = base?.coverage ?? settings.default_coverage; let enabled = base?.enabled ?? true; if ('coverageId' in field && field.coverageId) { const rawCov = byId(field.coverageId)?.value ?? ''; @@ -3636,8 +3634,11 @@ export async function saveGlobalSettings(e: Event): Promise { ]; if ('coverageId' in field && field.coverageId) { const el = byId(field.coverageId); - if (el) el.value = String(coverage); - savedSnapshot[field.coverageId] = getFieldValue(field.coverageId); + // Show the persisted (possibly propagated) value only when the card + // still holds what this Save read; an edit typed during the PUT stays + // dirty against the persisted value and goes out with the next Save. + if (el && getFieldValue(field.coverageId) === sentFields[field.coverageId]) el.value = String(coverage); + savedSnapshot[field.coverageId] = String(coverage); } }); });