Skip to content

fix(js): implement HTMLInputElement.indeterminate - #741

Open
ntdatt812 wants to merge 1 commit into
h4ckf0r0day:mainfrom
ntdatt812:fix/732-input-indeterminate
Open

fix(js): implement HTMLInputElement.indeterminate#741
ntdatt812 wants to merge 1 commit into
h4ckf0r0day:mainfrom
ntdatt812:fix/732-input-indeterminate

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 29, 2026

Copy link
Copy Markdown

Fixes #732.

What changed

indeterminate was not implemented anywhere in the engine. grep -r indeterminate crates/ returned nothing: no prototype accessor to back it, 'indeterminate' in checkbox was false, and a script could neither set nor read the flag.

crates/obscura-js/js/bootstrap.js

  • get/set indeterminate on the element prototype, backed by a node-keyed _formIndeterminate store. Node-keyed rather than per-instance for the same reason checked is: element wrappers are rebuilt on every lookup, so a field on the instance would not survive getElementById handing back a new wrapper. Being on the prototype is also what makes 'indeterminate' in el true for a freshly created element, which is the check in the issue's repro.
  • Element#click() clears indeterminate in the pre-click step and restores the old value when the click is canceled, alongside the existing checked handling.

crates/obscura-cdp/src/domains/input.rs

The pre-click activation logic is duplicated in the CDP mouse snippet, which is what real dispatched input goes through. Without the same three lines there, Input.dispatchMouseEvent on a checkbox would leave the flag stuck even once the property exists. This is the duplication the comment in bootstrap.js already points at.

Radio inputs are unaffected: HTML's pre-activation for radio sets checkedness to true and says nothing about indeterminateness, so the restore path there writes back the value it read.

One correction to the issue, which changes what the fix does

The issue also says an indeterminate checkbox clicked once should report checked=false, describing the state as "not flipped on the first click". That is not what HTML specifies. Legacy-pre-activation behavior is:

If this element's type attribute is in the Checkbox state, then set this element's checkedness to its opposite value (i.e. true if it is false, false if it is true) and set this element's indeterminateness to false.

Clearing indeterminate is an extra step, not a replacement for the toggle. An unchecked indeterminate checkbox reports checked=true, indeterminate=false after one click, which is what Chrome and Firefox do, and it is what this PR implements.

I have gone with the spec rather than the linked reference implementation because the spec is the thing the rest of the file follows. If you would rather match the reference, it is two lines in each of the two places, and I will send it as a follow-up rather than hold this one.

Validation

New test test_checkbox_indeterminate_is_idl_only_and_cleared_by_activation in crates/obscura-js/src/runtime.rs, covering the four things that were broken or at risk: 'indeterminate' in el is true on a fresh element and reads false; setting it round-trips; clicking an indeterminate checkbox yields checked=true, indeterminate=false; a canceled click (preventDefault) restores both.

I ran the four-configuration sequence from CONTRIBUTING rather than only the crate I touched, since this changes shared DOM and CDP code. All six steps in a rust:latest container, so the result matches CI rather than my Windows host:

Step Result
cargo build --release -p obscura-cli --bins --features render ok, 1m40s
cargo nextest run --release --features render --no-fail-fast 1507 passed, 4 skipped
cargo build --release -p obscura-cli --bins --no-default-features ok
cargo nextest run --release -p obscura --no-fail-fast 23 passed
cargo nextest run --release --workspace --exclude obscura --exclude obscura-render --no-default-features --no-fail-fast 758 passed, 3 skipped
cargo check --release -p obscura-render --no-default-features ok

The focused test, for the log:

PASS [0.017s] obscura-js runtime::tests::test_checkbox_indeterminate_is_idl_only_and_cleared_by_activation

I have not run cargo fmt over the tree, and the diff is the three files the fix needs.

Rendering

Not applicable. No layout, paint, screenshot, screencast or PDF code is touched, and the render-feature suite above is unchanged at 1507 passing.

Performance

The two accessors are only reached when a page reads or writes .indeterminate, which nothing did before this PR. The cost added to the common path is one object write per activation of a checkable input, next to the _formChecked write already there. Nothing is added to layout, paint, or the network path.

Checklist

  • The change is focused and does not remove existing behavior without justification.
  • Tests cover the failure or feature.
  • Existing tests pass, including render and no-render configurations when affected.
  • I checked for CPU, latency, and memory regressions.
  • Public API or user-facing behavior changes are documented.

The property was never defined anywhere in the engine, so
`'indeterminate' in checkbox` was false and scripts could neither set nor
read the flag. Add it as an IDL-only property backed by a node-keyed store,
the same way `checked` is stored, since element wrappers are rebuilt on
every lookup.

Checkbox activation now clears it too. HTML's legacy-pre-activation
behavior toggles checkedness and sets indeterminateness to false, and the
canceled path restores both. Two copies of that logic needed the change:
`Element#click()` in bootstrap.js, and the pre-click activation snippet in
obscura-cdp that drives real dispatched mouse input.

Fixes h4ckf0r0day#732.
@ntdatt812

Copy link
Copy Markdown
Author

Obstacle course, for the CONTRIBUTING pre-PR item I could not tick from a unit test. Companion repo at the ref your CI pins (6ebac82), same host, same invocation, base and candidate back to back:

revision correctness median latency
main (c138019) 32/33 3016.7ms
this branch 32/33 3016.1ms

No delta. The single failure is the same stage on both, and it fails on untouched main:

observer-intersection   modern-web   FAIL   expected 'io:50', got ''

So it is a property of this host rather than of the change, and I have not chased it further since it is outside the diff. It is deterministic rather than flaky here: re-running just that stage with --runs 3 --warmup 1 reproduces it exactly. Mentioning it only in case a 32/33 on a plain rust:latest Debian container is news to you; on your ubuntu-22.04 runner it may well be 33/33.

Everything else in this PR is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTMLInputElement.indeterminate is missing (and checkbox activation can't clear it)

1 participant