test(fields): drive the type="number" widgets with values a browser can deliver - #6850
Merged
Merged
Conversation
…an deliver
objectui#6765 pinned that happy-dom does not implement the HTML
value-sanitization algorithm, so a `fireEvent.change` in this package can put
text in `.value` that no browser ever puts there. That file records the
divergence deliberately, as one half of a disagreement.
Nothing recorded the other half: no test drove these widgets with the values a
real browser delivers and asserted the emission. The class had a pinned oracle
for a fiction and no oracle at all for the product.
Measured for this suite on this base — Chromium 141.0.7390.37 via Playwright
1.62.1, a real `<input type="number">` driven key by key, reading `.value` after
every keystroke:
typed "12abc" '' 1:'1' 2:'12' a:'12' b:'12' c:'12'
typed "1.2.3" '' 1:'1' .:'1' 2:'1.2' .:'1.2' 3:'1.23'
typed "0x10" '' 0:'0' x:'0' 1:'01' 0:'010'
typed "1e" '' 1:'1' e:'' <- validity.badInput becomes TRUE
typed "" ''
A browser delivers a SEQUENCE of post-sanitization readings, one per accepted
keystroke, not a single string — which is why the new suite drives arrays.
It also re-derives the card's quoted "6 of 10" instead of repeating it, extends
the class to `NumberField` and `GeolocationField` (which objectui#6765 never
measured), and carries a self-guard asserting that every string it drives is one
the platform itself reads without `validity.badInput`, so it cannot regress into
driving the fiction.
No widget source is touched.
Part of #6793
This was referenced Aug 30, 2026
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #6793
⛔ Deliberately not
Fixes. This delivers one half of that card and defers the other half to an unmerged sibling's file — see "What is deferred" below. Merging this must not close #6793.What this adds
One new test file,
packages/fields/src/__tests__/NumberInputWidgets.browserDeliverable.test.tsx, and an empty-frontmatter changeset. No widget source is touched, and no existing assertion moved.#6765 pinned that happy-dom does not implement the HTML value-sanitization algorithm, so a
fireEvent.changein this package can put text in.valuethat no browser ever puts there. That file records the divergence deliberately, as one half of a disagreement, and says so.Nothing recorded the other half: no test in this repository drove these widgets with the values a real browser delivers and asserted the emission. The class had a pinned oracle for a fiction and no oracle at all for the product. This file is that oracle.
What was measured, on this base
Chromium 141.0.7390.37 via Playwright 1.62.1 (
executablePath: '/opt/pw-browsers/chromium'), a real number input on a real page, driven key by key withkeyboard.press, reading.valueandvalidity.badInputafter every keystroke:Pasting (
keyboard.insertText) lands on the identical final readings. A programmatic.value = xwrite is a third route and lands on''for every one of these strings withbadInputFALSE — that route is #6780's subject, not this card's.⭐ The load-bearing shape: a browser does not deliver one post-sanitization string, it delivers a sequence of them, one per accepted keystroke.
"1e"makes it visible — the box moves'' -> '1' -> '', so React sees a real change and the widget is told to clear. Driving the single value''into a box that already reads''is not a change at all and emits nothing, which is a different fact about a different user action. That is why the new suite drives arrays.The per-case table, re-derived rather than quoted
old test droveis the whole fabricated string the existing pin feeds;browser deliversis the measured keystroke sequence;product emitsis what the real widget does when driven that way. All three columns measured here, not transcribed.12abc'12abc'12'1','12'121.2.3'1.2.3'1.2'1','1.2','1.23'1.230x10'0x10'0'0','01','010'101e'1e'1'1',''null''12abc'12abc'0.12'1','12'0.121.2.3'1.2.3'0.012'1','1.2','1.23'0.01230x10'0x10'0'0','01','010'0.11e'1e'0.01'1',''null''6 of 10 — re-derived, and it reproduces.
divergence.testasserts the six by name, computed by driving each case both ways through the real widget, so the figure cannot rot back into a quotation.⭐ Which side is wrong, per case: the oracle, in all six. The product reads what the browser hands it and gets the right answer on every row. Nothing in the widgets moved, and nothing should.
The two widgets the card names but #6765 never measured
12abcNaN'1','12'121.2.3NaN'1','1.2','1.23'1.230x1016'0','01','010'101eNaN'1',''null12abcNaN'1','12'121.2.3NaN'1','1.2','1.23'1.230x1016'0','01','010'101eNaN'1',''undefinedThese parse with
Number()rather thanparseFloat, so a fabricated string makes them emitNaN— a reading that would reach a form and a record. On every string a browser can actually deliver they agree with theparseFloatpair. The one asymmetry that is reachable —GeolocationFieldclearing toundefinedwhere the other three clear tonull— is pinned as current behaviour here and filed separately as #6848; it is not changed by this PR.The #6801 interaction, measured rather than reasoned
The same harness was run against
origin/mainand against PR #6801's head (adb3c3ace), on all 20 rows above:None of the six disagreements resolve when #6801 lands. That PR announces bad input (
validity.badInputto a refusal message, a border andaria-invalid) and states in its own source that it deliberately leaves every emission unchanged; the measurement agrees. So this card's deliverable is independent of that ruling, exactly as its triage said.The new suite was also run on #6801's tree, alongside that PR's own two suites:
3 files, 117 tests, all passing. It is forward-compatible by measurement, not by assertion.What is deferred, and why
Files PR #6801 modifies, checked one by one before anything was touched:
packages/fields/src/widgets/CurrencyField.tsxpackages/fields/src/widgets/PercentField.tsxpackages/fields/src/widgets/NumberField.tsxpackages/fields/src/widgets/GeolocationField.tsxpackages/fields/src/widgets/numberBadInput.tsxpackages/fields/src/__tests__/NumberInputWidgets.environmentDivergence.test.tsxpackages/fields/src/__tests__/numberInputBrowserReadings.tspackages/fields/src/__tests__/NumberInputWidgets.badInputAnnounce.test.tsxcontent/docs/**,packages/fields/README.md,.changeset/6780-*.mdThe ten fabricated-string cases live in
NumberInputWidgets.environmentDivergence.test.tsx, which PR #6801 modifies. Rewriting those bodies is the other half of #6793 and it is deferred to a follow-up on #6801 rather than raced here. The measured record this file carries also belongs, once #6801 lands, next to the numbers in that PR's newnumberInputBrowserReadings.ts; the docstring says so, and the keystroke table is a strict superset of that module'sBROWSER_READINGS(its last elements are those same readings), so the fold is mechanical rather than a reconciliation of two dialects.One claim in the card that does not survive re-derivation
Measured across every test file in this repository that renders
CurrencyField,PercentField,NumberFieldorGeolocationField(10 files): exactly one drives such a string —NumberInputWidgets.environmentDivergence.test.tsx, which exists to record the divergence. Every other one already drives'10','15','75','6000','2','1234.56','1234.567'— all browser-deliverable. The card flagged this as its own confidence gap 2 ("not a file count I have taken"), and the correction is written into the new file's docstring rather than left in a PR description.Ablation
Run on PR #6801's tree so no deferred file was mutated on this branch. Each leg: mutate, prove the mutation reached disk (a
grep -cFon the removed text and on the injected text, plusgit hash-objectagainst the HEAD blob), run, restore withgit checkout HEAD -- (absolute path), prove the restore with the blob hash and an emptygit diff HEAD. Both widgets are imported by relative source path, so vitest transforms the source directly — there is nodistin the resolution and no rebuild leg to get wrong.Verification
Union run after the final commit,
162c22566:The type-check is known to cover the new file rather than merely to have run: an earlier revision of it produced
TS2352from that exact path under the same command.lintwas run for@object-ui/fields(the only package this diff touches), not repo-wide.eslint.config.jsconfigures no type-aware rules (noproject/projectService), so a new file cannot move any untouched file's verdict. CI runs the full farm regardless.The changeset carries an empty frontmatter because the gate's own printed verdict named that as the answer for a change that releases nothing: no widget source is touched.
Generated by Claude Code