diff --git a/frontend/src/__tests__/plans.test.ts b/frontend/src/__tests__/plans.test.ts index a436840a3..e9591d981 100644 --- a/frontend/src/__tests__/plans.test.ts +++ b/frontend/src/__tests__/plans.test.ts @@ -3463,11 +3463,22 @@ describe('Plans Module', () => { expect(api.createPlannedPurchases).not.toHaveBeenCalled(); }); - test('weekly 20% plan currently prefills as immediate and previews 1 step (#1156)', async () => { - // Documents current behaviour of the Edit prefill, which maps weekly != 25% to `immediate`; see #1156. - // Invert this expectation (weekly 20% should preview 5 steps) when #1156 is fixed. - await openEdit(backendPlan({ type: 'weekly', percent_per_step: 20, step_interval_days: 7 })); - expect(points()).toBe(1); + const checkedRamp = (): string => document.querySelector('input[name="ramp-schedule"]:checked')!.value; + const customFields = (): string[] => ['ramp-step-percent', 'ramp-interval-days'].map(id => (document.getElementById(id) as HTMLInputElement).value); + + test.each([ + ['weekly 20% / 7d', { type: 'weekly', percent_per_step: 20, step_interval_days: 7 }, 'custom', ['20', '7'], 5], + ['monthly 5% / 30d', { type: 'monthly', percent_per_step: 5, step_interval_days: 30 }, 'custom', ['5', '30'], 20], + ['weekly 25% on a 14 day interval', { type: 'weekly', percent_per_step: 25, step_interval_days: 14 }, 'custom', ['25', '14'], 4], + ['monthly 10% on a 15 day interval', { type: 'monthly', percent_per_step: 10, step_interval_days: 15 }, 'custom', ['10', '15'], 10], + ['weekly 25% / 7d', { type: 'weekly', percent_per_step: 25, step_interval_days: 7 }, 'weekly-25pct', null, 4], + ['monthly 10% / 30d', { type: 'monthly', percent_per_step: 10, step_interval_days: 30 }, 'monthly-10pct', null, 10], + ['immediate', { type: 'immediate', percent_per_step: 100, step_interval_days: 0 }, 'immediate', null, 1], + ])('Edit prefills a stored %s ramp as %s (#1156)', async (_name, ramp, radio, fields, count) => { + await openEdit(backendPlan(ramp)); + expect(checkedRamp()).toBe(radio); + if (fields) expect(customFields()).toEqual(fields); + expect(points()).toBe(count); }); test('Edit with absent coverage shows the validation message and no points', async () => { diff --git a/frontend/src/plans.ts b/frontend/src/plans.ts index 55398c8a7..e9365d926 100644 --- a/frontend/src/plans.ts +++ b/frontend/src/plans.ts @@ -1272,6 +1272,19 @@ function openPlanForm(modal: HTMLElement, restartsFromToday = false): void { openModal(modal, { initialFocus: '#plan-name', onClose: () => { disposePlanEditor?.(); disposePlanEditor = undefined; } }); } +// Presets mirror config.PresetRampSchedules; any other stored ramp is shown as +// custom so Edit never rewrites it to a different schedule on Save (#1156). +const PRESET_RAMPS: ReadonlyArray<{ value: string; type: string; percent: number; intervalDays: number }> = [ + { value: 'weekly-25pct', type: 'weekly', percent: 25, intervalDays: 7 }, + { value: 'monthly-10pct', type: 'monthly', percent: 10, intervalDays: 30 }, +]; + +function rampRadioValue(ramp: { type?: string; percent_per_step?: number; step_interval_days?: number }): string { + if (ramp.type === 'immediate') return 'immediate'; + const preset = PRESET_RAMPS.find(p => p.type === ramp.type && p.percent === ramp.percent_per_step && p.intervalDays === ramp.step_interval_days); + return preset ? preset.value : 'custom'; +} + // editPlan loads the plan and opens the edit modal pre-filled. Returns true // when the modal opened, false when the plan could not be loaded (e.g. it was // deleted or is no longer accessible). Callers that render the plan in a list @@ -1284,15 +1297,7 @@ async function editPlan(planId: string): Promise { const info = extractPlanInfo(backendPlan); const rampSchedule = backendPlan.ramp_schedule || { type: 'immediate', percent_per_step: 100, step_interval_days: 0 }; - // Map ramp schedule type to frontend value - let rampValue = 'immediate'; - if (rampSchedule.type === 'weekly' && rampSchedule.percent_per_step === 25) { - rampValue = 'weekly-25pct'; - } else if (rampSchedule.type === 'monthly' && rampSchedule.percent_per_step === 10) { - rampValue = 'monthly-10pct'; - } else if (rampSchedule.type === 'custom' || (rampSchedule.type !== 'immediate' && rampSchedule.type !== 'weekly' && rampSchedule.type !== 'monthly')) { - rampValue = 'custom'; - } + const rampValue = rampRadioValue(rampSchedule); // Get payment option from services and normalize for provider. // H-5: do not pre-select 'no-upfront' when the payment field is absent. diff --git a/frontend/tests-e2e/plan-schedule-preview.spec.ts b/frontend/tests-e2e/plan-schedule-preview.spec.ts index bcbd9a22f..dbd2cf126 100644 --- a/frontend/tests-e2e/plan-schedule-preview.spec.ts +++ b/frontend/tests-e2e/plan-schedule-preview.spec.ts @@ -134,6 +134,14 @@ for (const [width, height] of [[1440, 900], [1280, 900], [1280, 768], [1024, 900 expect(footerBox.y + footerBox.height).toBeLessThanOrEqual(page.viewportSize()!.height); await page.locator('#close-plan-modal-btn').click(); } + plan.ramp_schedule = { type: 'weekly', current_step: 0, total_steps: 5, step_interval_days: 7, percent_per_step: 20 }; + await page.reload(); + await page.locator('[data-action="edit-plan"][data-id="draft-plan"]').click(); + await expect(page.locator('input[name="ramp-schedule"][value="custom"]')).toBeChecked(); + await expect(page.locator('#ramp-step-percent')).toHaveValue('20'); + await expect(page.locator('#ramp-interval-days')).toHaveValue('7'); + await expect(page.locator('#plan-editor-panel [data-preview-step]')).toHaveCount(5); + await page.locator('#close-plan-modal-btn').click(); await page.locator('#new-plan-btn').click(); const dense = page.locator('#plan-editor-panel'), scroll = dense.locator('.schedule-preview-scroll'); await page.locator('#plan-modal label.ramp-option').filter({ hasText: 'Define your own schedule' }).click();