1. Add a bunch of initXYZEvent functions, and fix the overload of initTextEvent
2. Add a number of missing (mostly legacy) accessors, e.g. uievent.getWhich()
3. CompositionEvent inherits from UIEvent, not Event
4. Give every accessor get/set a name. E.g the "name" of script.src is
"get script" and "set script".
Currently, we return a dummy `WebGLRenderingContext`. But this type implements
virtually nothing. From MDN: "If the context identifier is not supported null is
returned."
Now, normally we like to move things along as much as we can. Returning
`WebGLRenderingContext` lets us know the next thing the code needs. But in this
case, it seems to be problematic. At least some code is defensive around a null
value from getContext('webgl'), but once we return an instance, it expects it
to work correctly. This is what causes https://github.com/features/copilot to
enter an endless JS loop.
`Parser.attributeValue` walked quoted attribute values with
`indexOfScalarPos` to locate the closing quote and returned the raw byte
slice between the quotes — backslash escapes were neither honored as
escape boundaries nor decoded. As a result, `[data-x="abc\\def"]`
matched against the literal 8-byte string `abc\\def` instead of the
7-byte string `abc\def` the author intended, and `[data-x="foo\"bar"]`
truncated the value at the escaped inner quote.
Walk the string char-by-char respecting backslash escapes per CSS Syntax
Level 3 §4.3.5 (consume-string-token), reusing the existing
`parseEscape` helper that already powers `parseIdentifier` for `#id` /
`.class` selectors. Decode `\\`, `\"`/`\'`, and `\<hex digits>` (1–6
hex digits with optional whitespace terminator), drop `\<newline>` line
continuations, and surface bare newlines / trailing backslashes as
`InvalidAttributeSelector`.
Closes#2268
Per the WHATWG HTML "input type=range" value sanitization algorithm, an
out-of-range value assigned via the JS `value` setter (or `Element.value`)
must be clamped to `[min, max]`, with `min`/`max` defaulting to 0 and 100
when the attribute is missing or not a valid floating-point number; if the
assigned value isn't a valid floating-point number, fall back to
`min + (max - min) / 2`. The previous `.range` branch in `sanitizeValue`
only validated the float grammar and returned `"50"` as a hardcoded
default, so `<input type="range" min="0" max="100">; el.value = "999"`
left `el.value === "999"` instead of `"100"`.
Step matching is a separate sanitization rule and intentionally out of
scope.
Closes#2266
Per RFC 7231 §7.1.2, when a 3xx response carries a Location header
without a fragment, the user agent must process the redirect as if
the value inherited the fragment of the request URL. URL.resolve
follows RFC 3986 §5.3 which drops the base fragment, so handleRedirect
now reattaches the original fragment when the resolved target has none.
Closes#2263
Per the HTML Living Standard "constructing the form data set" algorithm,
a <select> element appends one entry per option whose selectedness is
true. A non-multiple <select> derives its selectedness from an
explicitly-selected option, falling back to the first non-disabled
option in tree order. With zero options (or only disabled options), no
option is selected, so no entry should be appended.
The singular-select fast path in collectForm previously broke from the
block with select.getValue(frame) regardless of whether any option
existed, causing FormData to emit a phantom (name, "") entry for an
empty <select>. Guard the fast path on the existence of at least one
non-disabled <option> child and skip the entry otherwise.
Closes#2262
Page.handleJavaScriptDialog previously responded -32000 "No dialog is
showing" regardless of whether a dialog was open, leaving CDP clients
no way to influence the JS-side return value of confirm() / prompt().
PR #2085 wired up the Page.javascriptDialogOpening event but explicitly
deferred the return-value override since true Chrome semantics require
suspending V8 mid-execution.
Add a pre-arm model that fits the auto-dismiss architecture without
runtime suspension: handleJavaScriptDialog stashes {accept, promptText}
on the BrowserContext; when the next JS dialog dispatches the
javascript_dialog_opening notification, the listener pops the stash and
fills it into the dispatch's response output param so Window.confirm /
prompt return the CDP client's choice. Without a pre-arm, headless
auto-dismiss values from PR #2085 are preserved (confirm->false,
prompt->null, alert->void).
Closes#2260
doReload built a NavigateOpts with only url + kind=.reload; method/body/header
defaulted to GET/null/null, so any prior POST navigation regressed to a GET
on reload. The HTML reload navigation re-fetches the document that produced
the current entry, and Chrome replays the same HTTP request that loaded the
page (including method, body, and Content-Type) — Lightpanda dropped all
three.
Retain the prior request body and content-type header in Frame.NavigatedOpts
(duped into the frame arena), and have doReload capture them into the CDP
command's arena before replacePage() frees the old frame. The reload's
frame.navigate call carries the replayed method/body/header so the request
the page was loaded with is the request that runs again.
Closes#2258
The JsApi binding for Location.prototype declared `pathname` and `search`
as read-only accessors (`null` setter), so assigning either had no
observable effect — the URL was never re-parsed and no navigation was
queued. Per HTML Living Standard §7.4.4.5.5/§7.4.4.5.6, both setters
must run the URL parser against a copy of the current URL and then
"Location-object navigate" to the result.
Add `setPathname` / `setSearch` member functions that mirror the
existing `setHash` shape: compute the new full URL via the matching
helper in `src/browser/URL.zig` and forward to `Frame.scheduleNavigation`
with `kind = .push` (matches Chrome's history-handling for these
setters). Wire them into the `pathname` / `search` accessors in place
of the previous `null` setters.
Closes#2256
Improvement to https://github.com/lightpanda-io/browser/pull/2246
In that PR, the check for `v8__Isolate__IsExecutionTerminating` guarded
runMicrotasks from running when the execution was terminated, but what if
termination happens immediately after? This adds a mutex to ensure that the
isolate cannot be terminated while microtasks are being processed.
The empty Zig anonymous struct `.{}` serializes to `[]` (tuple → JSON
array), not `{}`. The dispatch path's `InputParams.jsonParse` requires
the params field to begin with `object_begin`, so the previous test
fixtures hit `error.UnexpectedToken` → `error.InvalidJSON` instead of
exercising the production code paths.
Switch the two empty-params test cases to raw JSON string literals,
which the testing helper passes through unchanged (string literals are
pointer types and skip `std.json.Stringify.valueAlloc`). The production
code paths under test are unchanged.
`Network.clearBrowserCookies` had an inverted-logic guard that returned
`InvalidParams` whenever the caller included a `params: {}` field — which
most CDP libraries (chrome-remote-interface, chromedp, etc.) do
unconditionally. The CDP spec defines no parameters for the method, but
JSON-RPC convention is to silently accept extra ones; Chrome and the
sibling `Storage.clearCookies` handler already do. Drop the guard.
`Network.getAllCookies` was missing from the `Network` dispatch enum and
returned `UnknownMethod`. Add a small handler that returns the entire
cookie jar via the existing `CdpStorage.CookieWriter`, mirroring
`Storage.getCookies` minus the `browserContextId` filter (Network
commands are scoped to the current browser context already).
Closes#2254
When Form.requestSubmit() is called without a submitter argument, the
HTML spec defaults the internal submitter variable to the form element
itself (step 2). The subsequent "submit a form element" algorithm then
collapses submitter === form back to submitterButton = null before
constructing the SubmitEvent. We were skipping that collapse and using
the form as the SubmitEvent's submitter, which diverges from Chrome /
Firefox / WPT.
Frame.submitForm now coerces submitter_ to null when it equals the
form element, mirroring the spec algorithm. The form.html fixture
that previously asserted the wrong behavior is updated to assert
.submitter === null per spec.
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submitCloses#2252
Same-URL navigate should always cause a reload. The code that currently prevents
a navigate on fragment change is too loose and treats identical URLs as being
a fragment change..but for it to be considered a fragment change, the fragment
actually has to change.
This improves some WPT compat where tests do:
<iframe></iframe> <--- loads "about:blank"
<script>
document.querySelector('iframe').src = "about:blank"; <--- should load it again
</script>
Closes the test gap on the FrameSet stub. Asserts that
HTMLFrameSetElement exists as a global function, that <frameset> in
markup yields an HTMLFrameSetElement instance with the standard
tagName/toString/prototype-chain shape, and that document.createElement
('frameset') round-trips the same.
Refs #2249
Angular 9's zone.js polyfill calls HTMLFrameSetElement during
__load_patch, but the constructor was undefined in Lightpanda. The
fetch in #2249 dies on the very first patched API.
Add a minimal stub mirroring the existing HTMLDirectoryElement /
HTMLBodyElement pattern: a Zig struct with `_proto: *HtmlElement`,
asElement / asNode helpers, and a JsApi block exposing the bridge as
HTMLFrameSetElement. Registration goes in the same five files every
HTML element touches: the new file under
webapi/element/html/, an import + union field in Html.zig, the bridge
list in js/bridge.zig, the Tag enum + tag-name mappings in
webapi/Element.zig, and the default-display block list in
CSSStyleDeclaration.zig.
No tests; the build itself validates the registration paths.
Closes#2249
Runner._wait can iterate for the full opts.ms budget (up to 30s in
fetch, longer in agent tool-use loops). V8 was only nudged to GC on
session/page teardown (Browser.deinit, Page.deinit), so a page that
stays alive while running heavy JS accumulates wrappers and
external-ref'd Zig allocations V8 has no reason to drop. Fire
memoryPressureNotification(.moderate) once per second from the wait
loop.
The main.zig path for `fetch` now captures the *Browser so that
browser.env.terminate() can be called. This is a bit more complex than the serve
path because the Browser owns the Isolate and can't be moved from one thread to
another.
With main having access to the browser, two things are now possible:
1 - We can support a --terminate-ms flag (https://github.com/lightpanda-io/browser/issues/2206)
2 - ctrl-c can correctly stop blocked JavaScript processes
1 is implemented via setitimer to set a timer for SIGALRM, avoiding the need to
add another "watcher" thread, or putting a timer in Network.run.
Was previously using page.arena, but there's no reason to hold this beyond the
point where the page is parsed, and the page can live for quite some time after
the initial load.
Frame.getElementByIdFromNode is the selector engine's fast path for
`#id` queries. It only consulted `_elements_by_id`, while
Document.getElementById and ShadowRoot.getElementById both fall back
to `_removed_ids` + TreeWalker to recover a surviving duplicate after
the first has been removed.
The two APIs could disagree after DOM manipulation that removes a
duplicate-ID element (e.g. Turbo Drive's PageRenderer.replaceBody
doing innerHTML + replaceWith):
document.querySelector('#page-title') => null
document.getElementById('page-title') => <h1>
Dispatch to the existing canonical getElementById on the scope
(ShadowRoot or Document) instead of keeping a third, map-only copy
of the lookup. The two APIs now agree by construction.
Adds limited support for window.open. This leverages the new page container and
behaves similarly to an iframe. There are many things not implemented, but the
most significant are:
1- target=window_name or target=_blank don't work (but this could be added
pretty easily I think)
2- Windows (which are is just a Frame) are shutdown when the Page is shutdown.
They would need to be owned by the Session (rather than the Page), but I'm
not really confident in that right now.
3- No CDP testing. There are maybe CDP-specific messages we need to emit (or
maybe messages that we shouldn't emit).
As-is, this should help with common cases where:
1. window.open is called but doesn't matter (won't give a JS error)
2. window.open is short-lived, or, more specifically, lives only for the
duration of the page that opened it (e.g. a login popup).
3. WPT tests! (because most of these fit in #2).