diff --git a/frontend/src/__tests__/plans.test.ts b/frontend/src/__tests__/plans.test.ts index 188017121..18a015961 100644 --- a/frontend/src/__tests__/plans.test.ts +++ b/frontend/src/__tests__/plans.test.ts @@ -1441,16 +1441,51 @@ describe('Plans Module', () => { expect(chips?.textContent).not.toContain('Prod AWS'); }); - test('does not prefill when snapshot has more than one commitment', () => { - const second: api.Recommendation = { ...fixture, id: 'rec-771', service: 'rds' }; - openCreatePlanModal([fixture, second]); - - // populateTermSelect is called by setupRampScheduleHandlers / updateCommitmentOptions - // but NOT by prefillPurchaseConfigFromCommitment (which only runs for length===1) - // The provider/service select should NOT be forced to either rec's values - // We assert that the service select was not forced to 'ec2' alone - // (a multi-commitment plan requires manual selection) - expect(api.getAccount).not.toHaveBeenCalled(); + // #898: extend prefill to a homogeneous multi-commitment selection. The + // "Plan from N selected" button only enables when provider/service/term/ + // payment match across the selection, so the first commitment is a valid + // representative for the Purchase Configuration fields. + describe('prefill from multiple homogeneous commitments (#898)', () => { + test('prefills provider/service/term/payment from the shared values', () => { + const second: api.Recommendation = { ...fixture, id: 'rec-771', region: 'us-west-2' }; + openCreatePlanModal([fixture, second]); + + expect((document.getElementById('plan-provider') as HTMLSelectElement).value).toBe('aws'); + expect((document.getElementById('plan-service') as HTMLSelectElement).value).toBe('ec2'); + expect((document.getElementById('plan-term') as HTMLSelectElement).value).toBe('1'); + expect((document.getElementById('plan-payment') as HTMLSelectElement).value).toBe('partial-upfront'); + }); + + test('prefills the account chip when all commitments share one account', async () => { + (api.getAccount as jest.Mock).mockResolvedValueOnce({ + id: 'acct-uuid-123', + name: 'Prod AWS', + external_id: '123456789012', + }); + const second: api.Recommendation = { ...fixture, id: 'rec-771', region: 'us-west-2' }; + openCreatePlanModal([fixture, second]); + + await Promise.resolve(); + await Promise.resolve(); + + expect(api.getAccount).toHaveBeenCalledWith('acct-uuid-123'); + const hiddenIds = (document.getElementById('plan-account-ids') as HTMLInputElement).value; + expect(hiddenIds).toContain('acct-uuid-123'); + }); + + test('leaves the account chip empty when commitments span accounts', async () => { + const second: api.Recommendation = { ...fixture, id: 'rec-771', cloud_account_id: 'acct-uuid-999' }; + openCreatePlanModal([fixture, second]); + + await Promise.resolve(); + + // Config still prefills from the homogeneous fields, but the account + // is ambiguous so no chip is fetched. + expect((document.getElementById('plan-service') as HTMLSelectElement).value).toBe('ec2'); + expect(api.getAccount).not.toHaveBeenCalled(); + const hiddenIds = (document.getElementById('plan-account-ids') as HTMLInputElement).value; + expect(hiddenIds).toBe(''); + }); }); }); }); diff --git a/frontend/src/plans.ts b/frontend/src/plans.ts index 45d11f855..8d343d78a 100644 --- a/frontend/src/plans.ts +++ b/frontend/src/plans.ts @@ -836,8 +836,15 @@ async function setupPlanAccountsSection(planId?: string): Promise { /** * Prefill the Purchase Configuration section (provider / service / term / - * payment) from a single selected commitment. Called after form.reset() so - * the defaults are already in place; each field is still editable. (#770) + * payment) from a representative selected commitment. Called after + * form.reset() so the defaults are already in place; each field is still + * editable. (#770) + * + * For a multi-commitment selection the caller passes the first commitment as + * the representative: the "Plan from N selected" button is only enabled when + * the selection is homogeneous (same provider/service/term/payment — see + * isHomogeneousSelection in recommendations.ts), so any element shares those + * four values. (#898) */ function prefillPurchaseConfigFromCommitment(rec: api.Recommendation): void { const providerSelect = document.getElementById('plan-provider') as HTMLSelectElement | null; @@ -921,10 +928,13 @@ export function openCreatePlanModal(snapshot?: readonly api.Recommendation[]): v (document.getElementById('plan-id') as HTMLInputElement).value = ''; (document.getElementById('plan-form') as HTMLFormElement | null)?.reset(); - // When exactly one commitment is selected, prefill the Purchase - // Configuration fields so the user does not have to re-enter them. - // Fields are still fully editable after prefill. (#770) - if (pendingPlanRecommendations.length === 1) { + // When one or more commitments are selected, prefill the Purchase + // Configuration fields so the user does not have to re-enter them. The + // first commitment is the representative: for a multi-selection the + // "Plan from N selected" button only enables on a homogeneous selection, + // so every element shares provider/service/term/payment. Fields are still + // fully editable after prefill. (#770, #898) + if (pendingPlanRecommendations.length >= 1) { prefillPurchaseConfigFromCommitment(pendingPlanRecommendations[0]!); } @@ -943,13 +953,21 @@ export function openCreatePlanModal(snapshot?: readonly api.Recommendation[]): v planModalSession += 1; // setupPlanAccountsSection clears planSelectedAccounts and re-renders. - // When a single commitment carries a cloud_account_id, we look up that - // account after the section has reset and add it as a pre-selected chip. + // When every selected commitment carries the SAME cloud_account_id, we look + // up that account after the section has reset and add it as a pre-selected + // chip. Homogeneity of provider/service/term/payment (enforced by the + // "Plan from N selected" gate) does not imply a single account, so a + // multi-account selection leaves the chip empty for the user to fill. (#898) void setupPlanAccountsSection(); - if (pendingPlanRecommendations.length === 1) { - const accountId = pendingPlanRecommendations[0]!.cloud_account_id; - if (accountId) { - void prefillAccountChipFromId(accountId, planModalSession); + if (pendingPlanRecommendations.length >= 1) { + const firstAccountId = pendingPlanRecommendations[0]!.cloud_account_id; + const sharedAccountId = + firstAccountId && + pendingPlanRecommendations.every((r) => r.cloud_account_id === firstAccountId) + ? firstAccountId + : undefined; + if (sharedAccountId) { + void prefillAccountChipFromId(sharedAccountId, planModalSession); } }