https://github.com/lightpanda-io/browser/pull/3447 made better use of the
GlobalScope to simplify various callsites. This changes HttpClient.Owner to
contain the global_scope, rather than copying a handful of scope fields.
The RobotStore is shared by all Browsers. While every browser has a single
flight to prevent duplicate requests to the same robots.txt, that's limited to
that specific browser. So, 2 browsers can ask for the same robots.txt and then
put try to store the result. The RobotStore _is_ thread safe, but it's a simple
last-one-wins which overrites the previous record, without freeing either the
key or value.
This replaces the last-write-wins with a first-write-wins, avoiding the leak.
1. Add canvas getter to `OffscreenCanvasRenderingContext2D`, and cached
contet to OffscreenCanvas, aligning the OffscreenCanvas* with the non-Offscreens
2. Improve Request's stream support, draining on first use
3. Fix Request leak on error (unlikely to happen, but noticed the ordering was
wrong.)
1. and 2.were observed in the scraper logs...#1 happens _a lot_ (a wordpress
plugin).
xml5ever accepts processing instruction targets that the DOM rejects,
like <?1x?>. createProcessingInstruction then fails and returns null,
but xml5ever still appends the null node, and getNode segfaults.
Make NodeOrText.node optional and skip failed nodes in the append
callbacks. Parser.err is already set, so the XML parse returns a
<parsererror> document.
The agent-script tests only drive actions.click, so Input.dispatchMouseEvent's
own gate had no coverage: deleting the `if (!suppressed)` in triggerMousePress
left the whole suite green.
Press on a preventDefault-ing element and assert focus is kept, then press a
focusable one and assert focus moves, so the test pins the gate rather than an
inert default action. Verified to fail when the gate is removed.
The WebDriver path (performPointerSource) is still uncovered; WebDriver.zig has
no unit tests and is exercised through WPT testdriver, so that one needs a
harness rather than another test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Route the mouse-wheel, keypress and press paths through the new
EventManager.dispatchCancelable instead of open-coding the acquire-ref /
dispatch / read-defaultPrevented dance three more times. Use
getAttributeInterned for href/tabindex in the focusability helpers to match
their neighbors, and drop WHAT-restating comments.
Extract EventManager.dispatchCancelable so the acquire-ref/dispatch/read-
defaultPrevented pattern lives in one place instead of four copies across
actions.zig, user_input.zig and WebDriver.zig. Share a single
isNativelyFocusable predicate between the mouse-focus rules and moveFocus,
which had the same tag switch verbatim, and fold the SVG-link check into
isSvgLink. Return early from Element.focus when the element is already
active, before the visibility walk, so a click no longer pays a CSS
ancestor walk to re-focus a native control. Collapse the repeated MCP
click-test blocks into a selector loop and give the runtime focus test
active()/expectActive() helpers.
Addresses review on the mousedown default action.
Blur when a mousedown lands outside any focusable element: focusForMouseDown
now blurs the document's active element when the ancestor walk finds nothing
mouse-focusable, so a plain click moves focus to the body. The MCP fixture had
this backwards and asserted the old behavior; it now records
document.activeElement === document.body.
Treat an unparsable tabindex as if the attribute were absent (HTML 6.6.3)
rather than as a terminal "not focusable" answer, so <button tabindex="abc">
keeps its native focusability. This mirrors HtmlElement.getTabIndex's
parse-failure fallthrough for the same attribute instead of introducing a
second parser with different semantics.
Check the tabindex attribute before the HTML-only guard. An explicit,
parseable tabindex is focusable on any element, and SVG links are focusable by
the same href they activate on. This was only a gap until the blur above made
it a regression: clicking such an element used to leave focus alone and would
now have dropped it to the body, where Chrome focuses the element.
Honor preventDefault() on mousedown. The default action ran unconditionally,
so the toolbar idiom -- preventDefault() on mousedown to keep focus in a
focused input -- did not work, and the new blur made that load-bearing. The
mouse dispatch helpers in actions.zig, user_input.zig and WebDriver.zig now
report whether the event was cancelled, and each call site gates
focusForMouseDown on it. Non-mousedown call sites discard the result.
Editing-host focus behavior is deliberately unchanged.
Fold mouseFocusTabIndex into isMouseFocusable; its ?i32 return was never read.
Drop the duplicate Runtime.zig test that drove the same actions.click path as
the tools.zig fixture, and expand the remaining one, renamed to reflect what it
now covers. Two of its assertions sample document.activeElement in a mouseup
listener rather than after the click, because click activation behavior focuses
those elements unconditionally and would mask what mousedown decided. Harden
the disabled-control test to record the full five-event sequence instead of
mousedown alone. Every assertion was checked to fail against the pre-fix code.
zig build test 1340/1340, zig build test -Dwpt_extensions 1340/1340,
zig fmt --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main added custom property (`--*`) tracking to the same rule-building code
this branch made appendable. Both paths now share addSelectorRules, which
registers the custom declarations of a rule and its visibility rules, and
reports whether it added anything tracked. An insertRule append therefore
reaches custom_rules too, and clears the lazily parsed selectors of a
property it joins, instead of being dropped when the rule sets no
visibility property.
The raw-text path folds a block once for both kinds of declaration, so a
rebuild does not tokenize every block twice. rule_layers lives in the rule
arena so an appended rule can register its layer; the layered ranks are
still stamped by finalizeLayerRanks.
ScriptManager, XMLHttpRequest.zig, Fetch, Workers, etc. all take ownership (aka
dupe) the HTTP response from HTTPClient. They all have a headerCallback that
does something like:
```zig
if (transfer.getContentLength()) |cl| {
try self.body.ensureTotalCapacity(self.arena, cl);
}
```
But in all non-streaming cases (which is most cases), the HttpClient buffers
the response and only calls the headerCallback _after_ the body has been
received. Rather than relying on "Content-Length" header, the body buffer can
be sized to the exact body length. Why does this matter? Because the
Content-Length is the length of the body on the wire, and if the body is
compressed (like almost all .js files are), it will under-report the final
body length AND, because most callers are using an arena, the buffer growth
will retain more memory than it should.
This adds a `transfer.bodyLen()` method. Callers which dupe the body now use
this rather than the Content-Length (Content-Length is still used, e.g. for
XHR progress report).
Visibility probes no longer decide whether an element's style attribute
gets parsed into a CSSStyleProperties object. The three layout readers
(getElementAxis, positionStyle, horizontalPosition) create it on demand
through Element.inlineStyle, and StyleManager only ever folds the
attribute text. That removes the scan/materialize mode threaded through
every probe, and a JS layout read now only materializes the elements
whose inline style it actually reads.
The v8 documentation says this "Has to be called", and claude flagged it as a
potential leak.
Required a new zig-v8-fork build (v0.5.5) which I released.
Adds a `clutter` option to --strip-mode. This is based on readability.js. It
isn't a direct port (e.g. it doesn't strop bylines). It fallsback to `shell` if
it strips too much (and shell itself can fallback to not stripping anything).
But clutter rarely fallback to shell, only when a page is very small or when
it strips out _a lot_.
Also expanded shell to look at class names and ids.
Both MO and IO assumed an orderly transfer of its pending records to v8. But,
failure to transfer these objects to v8 (e.g. if the terminate flag was set)
means the records and their .tiny arenas were lost forever.
Some additional API changes:
- Add strip-mode support to pdf/png generation.
- Add LP.dump which provides greater content gathering capability to CDP,
exposing most `fetch` dump-related parameters (e.g. format, strip, selector,
...)
The new "--strip-mode shell" is designed to try to remove non-content elements
such as the header and footer. The end goal is to use readibility.js test cases
as a baseline, but this isn't a port of readibility.js.
This is just the basic implementation of this, e.g removing a few key tags, e.g.
<header>, <footer> and considering some specific roles.
Even if --strip-mode shell is used, we might decide to stick with a whole dump:
it's better to strip not enough than to strip too much. This currently works by
measuring the ratio of non-link text of the stripped vs unstripped page.
Debug and release builds each compiled their own copy of every C
dependency and of the Rust staticlib, because build.zig threaded the
top-level optimize mode into all of them. Under dev_fast the deps also
picked up the bundled-CRT target query, so even the same mode could not
share objects with a plain build.
Dependencies now build in ReleaseFast for the requested target, the way
the prebuilt V8 archive already works. Debug and release builds share
one set of cached dependency objects, and debug binaries run TLS, HTML
parsing, regex and sqlite optimized. -Ddebug_deps restores the old
behaviour for stepping into a dependency.
The Rust staticlib can only be shared by dropping the Debug-only memstats
feature: its single export, html5ever_get_memory_usage, was declared on
the Zig side but never called, and it pulled a jemalloc build into every
cold debug build. The Makefile override that existed for jemalloc's
nested make goes with it.
tighten socket ownership (on error paths)
allow reaper to be disabled
Handle window where link is being destroyed, worker is still alive, and client
attempts to re-link.
This is a small step towards WebDriver supports (non-bidi). It allows creating
and deleting a BiDi "Session" (e.g. a worker). It also allows attaching a BiDi
driver to an HTTP-created BiDi session (the typical selenium startup flow).
This change unblocks the most basic setup/teardown of Selenium, so it still
isn't enough to actually use a Selenium script as-is. But it's significant
because it models a worker (thread) that isn't tied to a WebSocket, something we
haven't had before.
A consequence of a pure HTTP Session is that we don't have a clear cleanup
signal. There is no "the socket is disconnected". There's a new HTTP reaper
which kills HTTP Sessions after --http-session-timeout. It's expected that
drivers properly DELETE /session/:id. I imagine we're going to run into
--cdp-max-connections limits and need to tweak this code. BUT, this entire flow
is only enabled with --protocol webdriver, so it won't impact exiting CDP users.
Three parameters every Playwright and Stagehand session sends were parsed
and then only logged as not implemented:
- Page.addScriptToEvaluateOnNewDocument runImmediately now also evaluates
the script in the current document, in the requested world.
- Emulation.setDeviceMetricsOverride screenWidth/screenHeight now back
window.screen, kept on the viewport override next to width/height; 0
keeps the current value, as for the other dimensions.
- Browser.setDownloadBehavior browserContextId is checked against the
loaded context, as the Storage commands already do.
Drivers focus a node before typing (chromedp's SendKeys calls DOM.focus,
then Input.dispatchKeyEvent). The method was unknown, so the keystrokes
went to the previously active element and every chromedp form fill was a
no-op. Resolve the node like the other DOM commands and call Element.focus,
which already handles focusability and the blur/focus event sequence.