axnodeWriter and axnodeQueryWriter both used session.currentFrame(),
but the root node may belong to a different frame (cross-frame query
from a parent context, iframe content). Name resolution (Label lookup
against ownerDocument) and visibility checks (frame._style_manager)
are per-frame, so the writer needs to bind to the node's owning frame.
Uses the existing Node.ownerFrame(fallback) helper. Fallback is
currentFrame for the orphan/detached-node case.
Also corrects a pre-existing latent bug in getFullAXTree where the
writer ignored the resolved frameId and used currentFrame instead.
Match Writer.writeNode (line 488-499): when a <label> targets a CSS-hidden
checkbox/radio (toggle-switch / CSS-only radio pattern), emit the label
with the input's role so clients searching by role land on the label's
clickable backendDOMNodeId rather than the hidden input.
Without this, queryAXTree(role="checkbox", name=...) on a toggle-switch
returns the display:none input — a non-actionable target.
Adds Accessibility.queryAXTree for finding AX nodes by role and/or
accessible name without serializing the full tree. Motivated by
agentic / MCP automation workloads where getFullAXTree round-trips
multi-MB JSON on complex pages.
QueryWriter walks the DOM subtree rooted at the requested node,
reuses the existing role + name resolution + ignore logic from
AXNode, and emits a flat array of matches. Reuses the same
VisibilityCache + LabelByForIndex + temp arena pattern as
axnodeWriter, so no extra retained state.
MVP limitations, each a small follow-up PR:
- objectId param returns a specific not-yet-supported error
- matches emit empty properties/childIds and no parentId
- frameId not parsed; queries the current frame
Implements the page-side surface of the W3C WebMCP spec
(https://webmachinelearning.github.io/webmcp/): exposes
`navigator.modelContext.registerTool(...)` for declaring MCP tools to a
browser agent, with full name/description validation, AbortSignal-based
unregistration, and a `ModelContextClient` whose `requestUserInteraction`
invokes its callback directly (closest faithful behavior in a headless
browser).
This allows pending destroys that have been accumulated to be cleaned up. In
normal operations, this likely isn't going to happen. But we see a some unit
tests create _many_ pages that never have the change to be cleaned up. The
result is that the next "normal" unit test, which actually runs enough through
Runner to trigger the cleanup, pays a huge cleanup price.
Arguably, for a test-only solution, we could create a session per test, or
have explicit cleanup in the test. But having 1 long-lasting session is useful
as it can show us these potential pitfalls AND, it isn't impossible that a
real-world case runs into similar issues.
CSSStyleSheet.insertRule previously detected at-rules in the parser
(has_skipped_at_rule) and returned the requested index without inserting
anything. The original change (PR #1972) did this to keep at-rule input
from killing module evaluation in apps like Expo Web -- correct, but the
silent-success contract has a second-order effect: CSS-in-JS libraries
(emotion, styled-components, Stitches, Mantine, Linaria) that round-trip
through cssRules to deduplicate their stylesheets see the rule as missing
after insertion, conclude the stylesheet is empty, and fall back to direct
<style> element injection per render. The result is unbounded <style>
accumulation on long-lived sessions doing repeated DOM interaction. See
issue #2459 for measurements (Allbirds-style PDPs accumulating thousands
of <style> elements over a render loop).
Changes:
* Parser.RulesIterator now returns a Rule union of {.style, .at_rule}
instead of skipping at-rules and setting has_skipped_at_rule. The
at-rule variant carries the keyword (without `@`) and the full source
span so callers can construct an opaque placeholder rule.
* CSSRule gains a `_text` field and an `initAtRule` constructor for
storing the at-rule source. CSSRule.getCssText returns the stored
text (CSSStyleRule's overridden getCssText still wins for `.style`
rules via the bridge dispatch on the most-derived class).
* CSSStyleSheet.insertRule and replaceSync handle both Rule variants:
regular rules become CSSStyleRule as before, at-rules become opaque
CSSRule placeholders with the matching CSSRule.Type variant
(vendor-prefixed keyframes are normalized; unrecognized at-rule
keywords fall back to .media). The CSS engine still doesn't apply
these rules -- that's intentional and outside the scope of this
change -- but they now surface via cssRules so library dedup paths
work correctly.
* StyleManager.addRawRules switches on the Rule kind and skips
at-rules (it only filters on display/visibility/opacity at the top
level, no semantic change).
* The CSSRule spec constants (STYLE_RULE, KEYFRAMES_RULE, MEDIA_RULE,
...) were declared as plain Zig consts inside JsApi but never wrapped
with bridge.property, so they came back as `undefined` from JS. Fixed
while writing the regression tests since the tests need to compare
rule.type against the spec constants.
Tests:
* Parser unit tests: cover statement at-rules (`@import url(...);`),
block at-rules (`@media`), and vendor-prefixed at-rules
(`@-webkit-keyframes`).
* HTML runner tests: cover insertRule for @keyframes, @media,
@supports, @font-face, vendor-prefixed at-rules, mixed style + at-rule
insertion, replaceSync at-rule preservation, and the dedup-via-cssRules
pattern that's the actual library code path the bug breaks.
A/B verification with the synthetic CSS-in-JS dedup pattern (50 calls to
inject the same @keyframes through a library that falls back to <style>
element injection when insertRule appears empty):
baseline (1.0.0-nightly.6240): styles=51 rules=0
patched (this branch): styles=1 rules=50
The leak collapses: instead of 50 <style> fallbacks stacking up, the
single persistent stylesheet receives all 50 insertions and dedup works.
Note on partial coverage of #2459: the original Allbirds reproducer
involves a Vue.js + Yotpo widget that injects <style> elements via
direct document.head.appendChild rather than insertRule. That code
path is unaffected by this change; it appears to be a separate
mechanism (possibly related to Vue's vue-style-loader closure-based
dedup or web-component lifecycle on Lightpanda) and is worth filing
separately. This PR fixes the specific insertRule contract issue
described in #2459 and unblocks the major CSS-in-JS libraries.
The submitForm encoding path was the last duplicate of the "limited to
only known values" canonicalization the previous commit consolidated for
the IDL getters. Now it consumes the same Form.normalizeMethod /
Form.normalizeEnctype helpers, so a single function owns the canonical
mapping (`""` / unknown -> spec default, recognized values pass through
unchanged).
Side effect of routing through the helper: the
`log.warn(.not_implemented, "FormData.encoding", ...)` branch falls out.
After commit 4b693db4 added `text/plain`, the only attribute values that
still reach the urlencoded fallback are spec-invalid ones, which per
HTML §4.10.21.5 silently canonicalize to
`application/x-www-form-urlencoded`. The warning was firing for valid
spec behavior — Chrome doesn't log either.
Behavior-preserving on all observable surfaces: full suite 639/639 green;
existing form-submission integration tests (multipart, urlencoded,
text/plain, GET-ignores-enctype) all pass unchanged.
Closing the divergence introduced by the new IDL accessors: `submitter.formEnctype`
(and `form.enctype`) now return "text/plain" for that attribute value per WHATWG
HTML §4.10.21.5, but `Frame.submitForm` previously fell back to urlencoded with
a `.not_implemented` log when it saw the same value on the submission path.
Implement the spec's text/plain encoding algorithm (HTML §4.10.21.8):
- FormData.EncType gains a `.plaintext` variant.
- FormData.plaintextEncode writes "name=value CRLF" per entry, no URL-encoding,
no escaping — the spec accepts that text/plain is a lossy, human-readable
encoding (values containing "=" or CRLF produce an ambiguous wire format
by design).
- Frame.submitForm recognizes "text/plain" before the urlencoded fallback and
sets the Content-Type header to "text/plain; charset=<form-charset>", per
spec step 21.4.
Two new Zig unit tests cover encoding output (`FormData: plaintext write`,
`FormData: plaintext empty body`). Full suite 639/639 green.
This is bundled with the IDL accessor commits because returning "text/plain"
from the IDL while the submission silently re-encodes as urlencoded is a
spec-internal inconsistency the IDL change itself introduces. Reviewers who'd
prefer to land just the read-only accessors first should feel free to ask for
a split — this commit is self-contained and reverts cleanly.
The "limited to only known values" canonicalization (per WHATWG HTML
§2.2.2) was duplicated five times: Form.getMethod + Form.getEnctype +
{Button,Input}.{getFormMethod,getFormEnctype}. Each callsite differed
only in the missing-value default ("" for submitter overrides, "get" /
"application/x-www-form-urlencoded" for the form-side).
Extract into two pub helpers on Form.zig taking the attribute slice +
the missing-value default. The five callers collapse to one-liners.
Behavior-preserving: existing form.html / button.html / input-attrs.html
fixtures all pass unchanged; full suite 637/637 green.
Net: -36 LOC.
Six form-submission IDL accessors were missing from the JsApi blocks of
HTMLFormElement, HTMLButtonElement, and HTMLInputElement, so reads
produced undefined instead of the spec-mandated string/boolean. The
content-attribute path (clicking a submit button honoring formaction /
formmethod / formenctype) was wired up in #2279; this commit adds the
matching IDL-property accessors per WHATWG HTML §4.10.18.6 and §4.10.21.5.
- Form.enctype: limited to known values, missing+invalid both default to
application/x-www-form-urlencoded (mirrors getMethod's shape).
- Button/Input formAction: returns frame.url when missing/empty, else the
resolved URL (mirrors Form.getAction).
- Button/Input formEnctype, formMethod: limited to known values with no
missing-value default ("" when missing, canonical invalid-value default
application/x-www-form-urlencoded / get when invalid).
- Button/Input formTarget: plain reflection, defaults to "".
- Button/Input formNoValidate: boolean reflection of formnovalidate.
Closes#2449
Remove no-longer needed setTimeouts in test now that messages are queued.
Runner also checks ready_queue when determining doneness.
Co-authored-by: Navid EMAD <design.navid@gmail.com>
At some point recently, we started to process scripts that fail to load (e.g.
404). This stops such scripts from [trying] to be evaluated, and executes the
onerror handler in all script loading paths.