Skip to content

fix(plans): live range/digit validation on plan-creation number fields - #714

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/702-plan-input-live-validation
May 25, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/702-plan-input-live-validation

Conversation

@cristim

@cristim cristim commented May 25, 2026 •

Copy link
Copy Markdown
Member

Closes #702. Five QA rows (3.11/3.16/3.17/3.19/3.24) where plan-creation number inputs accepted out-of-range or scientific-notation values until Save, then surfaced a backend error.

Changes

  • frontend/src/index.html:830 — added max="365" to #ramp-interval-days (previously had min="1" with no upper bound, so the up-arrow ran unbounded — covers row 3.17).
  • frontend/src/plans.ts — added two helpers:
    • wireRangeInput(inputId, min, max): input + blur listeners; rejects non-integer via /^\d+$/ (blocks 1e+30, decimals, negatives — covers row 3.19); toggles a sibling .field-error span; sets/clears aria-invalid. Uses a data-range-wired attribute for idempotency so re-opening the modal does not stack duplicate listeners.
    • wirePlanRangeInputs(): wires all four inputs — plan-coverage (0-100), ramp-step-percent (1-100), ramp-interval-days (1-365), plan-notify-days (1-30).
  • Called wirePlanRangeInputs() from openCreatePlanModal, openNewPlanModal, and editPlan.

Tests

  • 26 new tests in plans-range-validation.test.ts: out-of-range for each input, scientific-notation rejection, valid values clearing errors, blur triggers validation, idempotency, aria attributes.
  • Extended html.test.ts to assert ramp-interval-days has max="365".
  • 210 tests total pass. No backend changes needed (server already validates these fields).

Summary by CodeRabbit

Release Notes

  • New Features

    • Plan numeric inputs now enforce valid ranges with live error feedback and accessibility improvements.
  • Bug Fixes

    • Ramp interval input now limited to a maximum of 365 days.
  • Tests

    • Added comprehensive validation test coverage for plan creation numeric fields.

Review Change Stack

Add wireRangeInput helper to plans.ts and call it for all five numeric
inputs on the plan create/edit modal: plan-coverage (0-100),
ramp-step-percent (1-100), ramp-interval-days (1-365), plan-notify-days
(1-30). Validation fires on `input` and `blur`, shows a sibling
.field-error span, sets aria-invalid, and rejects scientific notation
via regex ^\d+$. Also adds max="365" to ramp-interval-days in HTML,
which was previously unbounded. Closes #702
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect labels May 25, 2026
@coderabbitai

coderabbitai Bot commented May 25, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d7198c7-6c87-4e5e-9365-f389592fe424

📥 Commits

Reviewing files that changed from the base of the PR and between 8035dbf and 22a4502.

📒 Files selected for processing (2)
  • frontend/src/__tests__/plans-range-validation.test.ts
  • frontend/src/plans.ts
📝 Walkthrough

Walkthrough

This PR implements live validation for five plan-creation numeric inputs to reject out-of-range, non-integer, and scientific-notation values inline. The max attribute constraint is added to the ramp-interval-days HTML input, and JavaScript event handlers are wired on modal open to validate against bounds and display accessible error messages.

Changes

Plan numeric field range validation

Layer / File(s) Summary
HTML constraints and validation entry points
frontend/src/index.html, frontend/src/plans.ts
The ramp-interval-days input gains max="365" to cap the field; wirePlanRangeInputs() is called in editPlan, openCreatePlanModal, and openNewPlanModal to activate live validation when the plan modal opens.
Range validation core logic
frontend/src/plans.ts
wireRangeInput and wirePlanRangeInputs helpers register input and blur listeners that validate whole-number strings (rejecting scientific notation via ^\d+$ regex), clamp parsed integers to [min, max], toggle aria-invalid, show/hide sibling error <small> elements, and update aria-describedby. An idempotency guard (data-range-wired) prevents duplicate listener registration across modal reopenings.
Range validation test suite
frontend/src/__tests__/plans-range-validation.test.ts
Comprehensive test module with synthetic plan modal DOM and mocked dependencies; parametrized tests cover out-of-range rejection, scientific-notation rejection, valid-value acceptance, field-clearing behavior, blur-triggered validation, stability across modal reopenings (no duplicate error spans), and accessibility compliance (error span has role="status" and aria-live="polite", inputs' aria-describedby includes error span id).
HTML attribute validation
frontend/src/__tests__/html.test.ts
Test assertion confirms ramp-interval-days input has min="1" and max="365" attributes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hop and skip through numbers true,
Min and max now bound the view,
No more sprawling to infinity,
Live validation sets us free,
Range is right, the form knows best!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding live validation for plan numeric inputs with range and digit constraints.
Linked Issues check ✅ Passed The PR fully addresses issue #702: adds max="365" to ramp-interval-days, implements live range/digit validation via wireRangeInput helper, rejects scientific notation using /^\d+$/ regex, and wires validation for all five specified inputs at modal open points.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #702: HTML bounds, JS validation helpers, modal wiring, and comprehensive tests covering out-of-range, scientific notation, accessibility (aria-invalid, aria-describedby, aria-live), and idempotency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/702-plan-input-live-validation

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/__tests__/plans-range-validation.test.ts`:
- Line 62: The mock's showToast callback currently has an untyped parameter
(opts) causing TS7006; annotate opts to a concrete type to satisfy
noImplicitAny—either import/use the proper toast option type from your toast
utilities or derive it from the existing mock using a helper like
Parameters<typeof mockShowToast>[0]; update the showToast entry so it reads
showToast: (opts: <derived-or-imported-type>) => mockShowToast(opts) referencing
mockShowToast to locate the mock.

In `@frontend/src/plans.ts`:
- Around line 947-955: The validation marks out-of-range or non-integer values
using integerPattern, raw, min, max, input and error but doesn't clamp them;
update the logic in the same block so that when parseInt(raw, 10) < min or > max
you clamp the numeric value to the nearest bound and write the clamped string
back into input.value (use a single parsed value variable to avoid
double-parsing), and still set aria-invalid and error text when the original raw
was invalid; ensure integerPattern continues to guard non-integer input before
clamping.
- Around line 918-920: The idempotency guard using input.dataset['rangeWired']
causes early return and leaves any existing .field-error UI unchanged; change
the logic so that if input.dataset['rangeWired'] is set you do not re-bind event
listeners but you DO run the validation/cleanup path (clear or re-run the
validation that toggles .field-error for this input) before returning, or
alternatively remove the early return and only skip addEventListener calls while
still invoking the function that syncs error state; reference
input.dataset['rangeWired'] and ensure the code that toggles .field-error for
the input is executed on each modal open/rebind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1904262b-aebb-445a-8d8f-eb36612e3ad6

📥 Commits

Reviewing files that changed from the base of the PR and between f3240f4 and 8035dbf.

📒 Files selected for processing (4)
  • frontend/src/__tests__/html.test.ts
  • frontend/src/__tests__/plans-range-validation.test.ts
  • frontend/src/index.html
  • frontend/src/plans.ts

Comment thread frontend/src/__tests__/plans-range-validation.test.ts Outdated
Comment thread frontend/src/plans.ts Outdated
Comment thread frontend/src/plans.ts Outdated
Finding 1 (test line 62, TS7006): annotate the showToast mock callback
parameter as ToastOptions (imported from '../toast') instead of implicit
any. Both the jest.fn implementation and the factory callback are now
explicitly typed, satisfying noImplicitAny without using a widening
unknown that would lose the shape.

Finding 2 (plans.ts ~918): the idempotency guard exited early without
re-validating the current field value, so stale .field-error / aria-invalid
UI could persist across modal reopens. Fix: dispatch a synthetic 'input'
event before returning so the existing check listener reconciles the error
state on every reopen.

Finding 3 (plans.ts ~955): out-of-range integers were flagged but never
clamped, leaving invalid raw values in the field. Fix: add a clampOnBlur
listener that silently clamps integer values to [min, max] on blur and
clears the error; non-integer values (scientific notation, fractions) are
skipped by the guard so the error stays visible. The inline check function
now separates the integer-pattern branch from the range branch to avoid
double-parsing and allow a clean early return.

New tests: blur-clamps-to-max, blur-clamps-to-min, non-integer not clamped
on blur, stale error reconciled on modal reopen (19 tests total).

Refs #702
@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

Resolved all 3 actionable CR findings in commit 22a4502.

Finding 1 - TS7006 implicit any in showToast mock (plans-range-validation.test.ts:62):
Added import type { ToastOptions } from '../toast' and typed both the jest.fn implementation parameter (_opts: ToastOptions) and the factory callback (opts: ToastOptions). This is more precise than unknown and eliminates the TS7006 error while preserving the toast shape for future type-checking.

Finding 2 - Stale error UI on modal reopen (plans.ts:918):
The idempotency guard now dispatches a synthetic 'input' event before returning, causing the already-registered check listener to reconcile .field-error / aria-invalid state against the current field value on every modal reopen. Added test: 're-opening the modal reconciles stale error UI for valid default values'.

Finding 3 - Out-of-range values flagged but never clamped (plans.ts:955):
Implemented a clampOnBlur listener (registered on blur) that silently clamps integer values to [min, max] and clears the error after the user leaves the field. The check listener (on input) still shows the inline error while typing so the user sees the boundary. Non-integer values (scientific notation, fractions) are skipped by the pattern guard in clampOnBlur so their error persists. The check function was refactored to separate the integer-pattern branch from the range branch, removing double-parsing. Added tests: blur-clamps-to-max, blur-clamps-to-min, non-integer not clamped on blur.

Total tests: 19 (up from 15). TypeScript clean (tsc --noEmit passes).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

@cristim: Thank you for the thorough write-up! All three fixes look well-considered:

  • Finding 1: Typing the mock with ToastOptions is cleaner than unknown — it keeps the mock honest and helps catch shape mismatches in future tests.
  • Finding 2: Dispatching a synthetic input event on re-entry is a neat way to let the already-registered listener self-reconcile without duplicating validation logic.
  • Finding 3: The separation of check (on input, shows error while typing) from clampOnBlur (on blur, silently corrects) is a good UX pattern — users see feedback while typing but end up with a valid value after leaving the field. Skipping the clamp for non-integers so their error persists is the right call.

Triggering a review of the updated commit.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 7ea945c into feat/multicloud-web-frontend May 25, 2026
5 checks passed
@cristim
cristim deleted the fix/702-plan-input-live-validation branch May 25, 2026 23:14
cristim added a commit that referenced this pull request May 28, 2026
…776)

Add wireRangeInput('add-purchases-count', 1, 52) in openAddPurchasesModal
so the Number of Purchases field shows a "Must be a whole number between
1 and 52" error inline as the user types, instead of only at save time.
The submit button is also disabled while the field is aria-invalid.

Harden the save-time guard in handleAddPurchases to use Number() +
Number.isInteger() instead of parseInt, so a fractional value like 2.5
is rejected before reaching the API (feedback_strict_int_parse).

Export openAddPurchasesModal for test access and add 13 new cases in
plans-range-validation.test.ts covering below-range (0), above-range
(53), boundary values (1/26/52), fractions (2.5), button-disabled
toggling, a11y attributes, and the save-time guard.

Mirrors the wireRangeInput pattern from #702/#714 across the codebase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant