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
21 changes: 16 additions & 5 deletions frontend/src/__tests__/plans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>('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 () => {
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1284,15 +1297,7 @@ async function editPlan(planId: string): Promise<boolean> {
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.
Expand Down
8 changes: 8 additions & 0 deletions frontend/tests-e2e/plan-schedule-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading