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
10 changes: 10 additions & 0 deletions FIDELITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ The committed PNGs under `testdata/renders/` back every claim here. Reproduce
them with the commands at the bottom. The measured-vs-Chrome numbers live in
[`bench/REPORT.md`](bench/REPORT.md).

## 2026-09-28 (round 139) — the Constraint Validation API (`setCustomValidity`/`.validity`/`.validationMessage`/`checkValidity`) was entirely missing — CONFIRMED extensive real usage on github.com's own client-side form-validation UI (username/label/2FA fields) (engine#239)

Continued sweeping the same fresh github.com bundles round 138 found `.indeterminate` in, this time for method calls rather than property assignments. `.checkValidity(` (7 hits) and `.setCustomValidity(` (4 hits) stood out immediately, alongside `.validity.customError`/`.validationMessage` reads — GitHub's own form-validation UI relies on this API extensively for username/label/2FA-code field checks.

- **Scoped precisely to what's evidenced, not the full spec surface**: this engine models NO native constraints at all — no `required`/`pattern`/`min`/`max`/`step` checking against a value. The corpus usage is exclusively the CUSTOM-validity pattern (`setCustomValidity(msg)` / `setCustomValidity("")` / reading `.validity.customError`/`.validationMessage` / gating on `.checkValidity()`), so that's what's implemented — but the `ValidityState` object's SHAPE is spec-complete (all nine other flags — `valueMissing`, `typeMismatch`, `patternMismatch`, `tooLong`, `tooShort`, `rangeUnderflow`, `rangeOverflow`, `stepMismatch`, `badInput` — are present and always `false`, not silently `undefined`), so a script that checks any of them gets a real, defined answer rather than a crash.
- **Fixed** (`dom.Node.CustomValidity string`, the same non-attribute-runtime-field shape as round 138's `Indeterminate`; `js/dom.go`): `setCustomValidity(msg)` sets it; `.validationMessage` and `.validity.customError`/`.valid` read it; `checkValidity()` returns `validity.valid` and, per spec, fires a cancelable `"invalid"` event at the element when invalid.
- **`.willValidate` deliberately left out**: zero corpus usage found for it anywhere in this session's fetched scripts, unlike the four members above.
- **One new test**, git-stash-confirmed: reverting makes the test's own script throw `TypeError: Cannot read property 'valid' of undefined` — the same class of failure rounds 124-126 documented for a missing method.
- **Bench**: flat/within already-documented noise across all ten pages, including github.com/golang/go itself (0.638, unchanged) — expected, pure JS-correctness fix with no paint-visible effect by design. All coverage floors held.

## 2026-09-28 (round 138) — `HTMLInputElement.indeterminate` and the `:indeterminate` CSS pseudo-class were entirely missing — CONFIRMED real corpus usage on github.com's own "select all" bulk-action checkboxes (engine#238)

Broadened the corpus sweep to a genuinely fresh page (github.com's own homepage and its `behaviors.js`/`element-registry.js`/`environment.js` bundles, not previously in this session's ten-page set), after the existing corpus stopped turning up new evidenced gaps. `.indeterminate=` was the one new real hit: `(0,v.l)("[data-indeterminate]",{constructor:HTMLInputElement,initialize(e){e.indeterminate=!0}})` — GitHub's own tri-state "select all" checkbox pattern.
Expand Down
22 changes: 11 additions & 11 deletions bench/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- AUTO-GENERATED: this header, the results table and the montage list are regenerated by `go run ./cmd/compare`. The hand-written analysis below the BEGIN ANALYSIS marker is preserved across re-runs. -->

**Generated:** 2026-09-28 06:24 UTC
**Generated:** 2026-09-28 06:39 UTC
**Viewport:** 1024×768, device-scale 1
**Timing:** median of N=5 runs after 1 warmup, wall-clock incl. network fetch
**Chrome:** `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
Expand All @@ -11,16 +11,16 @@
<!-- BEGIN RESULTS TABLE -->
| URL | SSIM | pixdiff % | webengine ms | chrome ms | speed× | region | status |
|-----|-----:|----------:|-------------:|----------:|-------:|:------:|:------:|
| example.com/ | 0.954 | 1.5 | 52.3 | 1283.0 | 24.52 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.436 | 21.3 | 2699.3 | 3880.0 | 1.44 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.716 | 11.3 | 5461.5 | 3951.6 | 0.72 | 1024×2500 | ok |
| go.dev/blog/ | 0.690 | 16.7 | 4432.9 | 1995.8 | 0.45 | 1024×1439 | ok |
| react.dev/ | 0.725 | 33.0 | 3436.6 | 2266.9 | 0.66 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.603 | 14.4 | 1233.1 | 2098.7 | 1.70 | 1024×1077 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.606 | 18.1 | 540.9 | 1522.4 | 2.81 | 1009×2500 | ok |
| github.com/golang/go | 0.638 | 14.2 | 2607.3 | 1733.7 | 0.66 | 1024×2491 | ok |
| tailwindcss.com/ | 0.740 | 11.3 | 7475.3 | 1868.8 | 0.25 | 1024×2500 | ok |
| caniuse.com/ | 0.657 | 18.2 | 4899.5 | 3228.2 | 0.66 | 1024×1740 | ok |
| example.com/ | 0.954 | 1.5 | 52.5 | 1296.1 | 24.69 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.436 | 21.3 | 2701.7 | 3826.7 | 1.42 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.716 | 11.3 | 5235.0 | 3904.4 | 0.75 | 1024×2500 | ok |
| go.dev/blog/ | 0.690 | 16.7 | 4432.5 | 1879.3 | 0.42 | 1024×1439 | ok |
| react.dev/ | 0.725 | 33.0 | 3295.5 | 2173.4 | 0.66 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.601 | 14.4 | 1238.8 | 2056.2 | 1.66 | 1024×1077 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.606 | 18.1 | 524.6 | 1435.1 | 2.74 | 1009×2500 | ok |
| github.com/golang/go | 0.638 | 14.2 | 2404.3 | 1712.0 | 0.71 | 1024×2491 | ok |
| tailwindcss.com/ | 0.739 | 11.3 | 7522.3 | 1778.8 | 0.24 | 1024×2500 | ok |
| caniuse.com/ | 0.658 | 18.1 | 4805.9 | 3331.1 | 0.69 | 1024×1740 | ok |
<!-- END RESULTS TABLE -->

Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.
Expand Down
Binary file modified bench/out/caniuse.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/github.com_golang_go.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/news.ycombinator.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/tailwindcss.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 38 additions & 38 deletions bench/results.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"url": "https://example.com/",
"webengine_ms": 52.324,
"chrome_ms": 1282.957,
"speed_ratio": 24.519474810794286,
"webengine_ms": 52.504,
"chrome_ms": 1296.099,
"speed_ratio": 24.68571918330032,
"ssim": 0.9541451179596087,
"pixdiff_pct": 1.53350830078125,
"region_w": 1024,
Expand All @@ -14,9 +14,9 @@
},
{
"url": "https://en.wikipedia.org/wiki/Go_(programming_language)",
"webengine_ms": 2699.322,
"chrome_ms": 3879.953,
"speed_ratio": 1.4373805718621193,
"webengine_ms": 2701.66,
"chrome_ms": 3826.699,
"speed_ratio": 1.4164250867984869,
"ssim": 0.4361758822140753,
"pixdiff_pct": 21.279570312500002,
"region_w": 1024,
Expand All @@ -27,9 +27,9 @@
},
{
"url": "https://pkg.go.dev/net/http",
"webengine_ms": 5461.531,
"chrome_ms": 3951.595,
"speed_ratio": 0.7235324673612582,
"webengine_ms": 5234.99,
"chrome_ms": 3904.379,
"speed_ratio": 0.745823583235116,
"ssim": 0.7155686319990039,
"pixdiff_pct": 11.3370703125,
"region_w": 1024,
Expand All @@ -40,9 +40,9 @@
},
{
"url": "https://go.dev/blog/",
"webengine_ms": 4432.901,
"chrome_ms": 1995.772,
"speed_ratio": 0.4502180400599968,
"webengine_ms": 4432.459,
"chrome_ms": 1879.311,
"speed_ratio": 0.4239883549966283,
"ssim": 0.6901862268272003,
"pixdiff_pct": 16.726839384989574,
"region_w": 1024,
Expand All @@ -53,9 +53,9 @@
},
{
"url": "https://react.dev/",
"webengine_ms": 3436.649,
"chrome_ms": 2266.886,
"speed_ratio": 0.6596210436387306,
"webengine_ms": 3295.483,
"chrome_ms": 2173.381,
"speed_ratio": 0.6595030227738998,
"ssim": 0.72456508425769,
"pixdiff_pct": 32.97015625,
"region_w": 1024,
Expand All @@ -66,11 +66,11 @@
},
{
"url": "https://news.ycombinator.com/",
"webengine_ms": 1233.055,
"chrome_ms": 2098.676,
"speed_ratio": 1.7020132921889128,
"ssim": 0.6033293622898777,
"pixdiff_pct": 14.418306058495823,
"webengine_ms": 1238.799,
"chrome_ms": 2056.235,
"speed_ratio": 1.6598616886193807,
"ssim": 0.6013611994886636,
"pixdiff_pct": 14.430819115598887,
"region_w": 1024,
"region_h": 1077,
"montage": "out/news.ycombinator.com.png",
Expand All @@ -79,9 +79,9 @@
},
{
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS",
"webengine_ms": 540.939,
"chrome_ms": 1522.391,
"speed_ratio": 2.8143487528168616,
"webengine_ms": 524.552,
"chrome_ms": 1435.116,
"speed_ratio": 2.7358889109182694,
"ssim": 0.6056768401997266,
"pixdiff_pct": 18.121902874132807,
"region_w": 1009,
Expand All @@ -92,11 +92,11 @@
},
{
"url": "https://github.com/golang/go",
"webengine_ms": 2607.312,
"chrome_ms": 1733.675,
"speed_ratio": 0.6649280945280043,
"ssim": 0.63813074167239,
"pixdiff_pct": 14.235309614612603,
"webengine_ms": 2404.346,
"chrome_ms": 1711.988,
"speed_ratio": 0.7120389494690033,
"ssim": 0.6381281554378134,
"pixdiff_pct": 14.235623243677237,
"region_w": 1024,
"region_h": 2491,
"montage": "out/github.com_golang_go.png",
Expand All @@ -105,11 +105,11 @@
},
{
"url": "https://tailwindcss.com/",
"webengine_ms": 7475.3,
"chrome_ms": 1868.777,
"speed_ratio": 0.24999357885302262,
"ssim": 0.7396458938927728,
"pixdiff_pct": 11.30765625,
"webengine_ms": 7522.284,
"chrome_ms": 1778.841,
"speed_ratio": 0.23647618196813627,
"ssim": 0.7388802364988983,
"pixdiff_pct": 11.3133984375,
"region_w": 1024,
"region_h": 2500,
"montage": "out/tailwindcss.com.png",
Expand All @@ -118,11 +118,11 @@
},
{
"url": "https://caniuse.com/",
"webengine_ms": 4899.46,
"chrome_ms": 3228.213,
"speed_ratio": 0.6588915921346434,
"ssim": 0.6569742927777596,
"pixdiff_pct": 18.19190014367816,
"webengine_ms": 4805.943,
"chrome_ms": 3331.103,
"speed_ratio": 0.6931216204603342,
"ssim": 0.6579995177089225,
"pixdiff_pct": 18.11444863505747,
"region_w": 1024,
"region_h": 1740,
"montage": "out/caniuse.com.png",
Expand Down
12 changes: 12 additions & 0 deletions dom/dom.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ type Node struct {
// `:indeterminate` CSS pseudo-class (css/selector.go).
Indeterminate bool

// CustomValidity holds a form control's own "custom error message" set by
// script via `.setCustomValidity(msg)` (HTML Standard's Constraint
// Validation API) — empty means no custom error. This engine models NO
// native constraints at all (no required/pattern/min/max/step checking
// against a value), so this is the ONLY thing that can ever make
// `.validity.valid`/`.checkValidity()` false — a deliberate, disclosed
// scope boundary: this backs the real, evidenced usage (see js/dom.go's
// own doc comment), not a full constraint-validation engine. Runtime-only
// field, the same shape as Indeterminate above — there is no
// "customvalidity" content attribute.
CustomValidity string

// Shadow is the shadow root attached to this element (a declarative
// <template shadowrootmode> hoisted out at parse time — see
// attachDeclarativeShadowRoots), or nil for a plain element. When set,
Expand Down
42 changes: 42 additions & 0 deletions js/dom.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,48 @@ func (b *binder) defineElement(o *goja.Object, n *dom.Node) {
b.accessor(o, "indeterminate",
func() goja.Value { return b.vm.ToValue(n.Indeterminate) },
func(v goja.Value) { n.Indeterminate = v.ToBoolean() })
// The Constraint Validation API (HTML Standard §4.10.21.3) was entirely
// missing. This engine models NO native constraints at all — no
// required/pattern/min/max/step checking against a value — so a
// CUSTOM validity message (dom.Node.CustomValidity, its own doc comment
// explains why) is the only thing that can ever make `.validity.valid`
// false; every other ValidityState flag (valueMissing, typeMismatch,
// patternMismatch, tooLong, tooShort, rangeUnderflow, rangeOverflow,
// stepMismatch, badInput) is always false, a disclosed scope boundary,
// not silently wrong — the object's SHAPE is spec-complete (a script
// reading any of them gets a real boolean, never undefined) even though
// the underlying checks are not modelled. Real, extensive corpus usage
// confirmed on github.com's own behaviors.js: its client-side form
// validation UI (username/label/2FA-code fields) calls
// `input.setCustomValidity(msg)` to mark a field invalid with a message,
// `""` to clear it, reads `input.validity.customError`/
// `.validationMessage` to decide what to show, and gates form submission
// on `form.checkValidity()`.
b.accessor(o, "validationMessage", func() goja.Value { return b.vm.ToValue(n.CustomValidity) }, nil)
b.accessor(o, "validity", func() goja.Value {
valid := n.CustomValidity == ""
v := b.vm.NewObject()
for _, k := range []string{"valueMissing", "typeMismatch", "patternMismatch", "tooLong",
"tooShort", "rangeUnderflow", "rangeOverflow", "stepMismatch", "badInput"} {
v.Set(k, false)
}
v.Set("customError", !valid)
v.Set("valid", valid)
return v
}, nil)
o.Set("setCustomValidity", func(call goja.FunctionCall) goja.Value {
n.CustomValidity = call.Argument(0).String()
return goja.Undefined()
})
o.Set("checkValidity", func(call goja.FunctionCall) goja.Value {
valid := n.CustomValidity == ""
if !valid {
ev := b.newEvent("invalid")
ev.Set("cancelable", true)
b.dispatch(n, "invalid", ev)
}
return b.vm.ToValue(valid)
})
// HTMLDetailsElement.open — a plain boolean reflection (HTML Standard
// §4.11.1), the same presence-based shape as checked/hidden above — was
// entirely missing, so `details.open = true` from script silently
Expand Down
32 changes: 32 additions & 0 deletions js/js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,38 @@ func TestIndeterminateAccessor(t *testing.T) {
"afterUnset=false")
}

// TestConstraintValidationAPI confirms setCustomValidity/validity/
// validationMessage/checkValidity — entirely missing before this fix — the
// Constraint Validation API subset this engine actually models: a CUSTOM
// validity message is the only thing that can ever make an element invalid
// (no native required/pattern/min/max/step checking is modelled at all, a
// disclosed scope boundary), and checkValidity() fires a cancelable
// "invalid" event when invalid, per spec. Real corpus usage confirmed on
// github.com's own behaviors.js form-validation UI.
func TestConstraintValidationAPI(t *testing.T) {
_, logs, _ := runJS(t, page(`
var d = document.getElementById('d');
console.log('defaultValid='+d.validity.valid+' customError='+d.validity.customError+' msg='+JSON.stringify(d.validationMessage));
console.log('defaultCheck='+d.checkValidity());

var invalidFired = false;
d.addEventListener('invalid', function(e){ invalidFired = true; console.log('invalidCancelable='+e.cancelable); });
d.setCustomValidity('nope');
console.log('afterSet valid='+d.validity.valid+' customError='+d.validity.customError+' msg='+d.validationMessage);
console.log('afterSetCheck='+d.checkValidity()+' fired='+invalidFired);

d.setCustomValidity('');
console.log('afterClear valid='+d.validity.valid+' customError='+d.validity.customError);
`))
mustHave(t, logs,
`defaultValid=true customError=false msg=""`,
"defaultCheck=true",
"afterSet valid=false customError=true msg=nope",
"afterSetCheck=false fired=true",
"invalidCancelable=true",
"afterClear valid=true customError=false")
}

// TestAddEventListenerOnce confirms addEventListener's `{once: true}` option
// — entirely ignored before this fix (the third argument was never read at
// all) — per the DOM standard's own "inner invoke" algorithm (§2.9): a once
Expand Down
Loading