Requires: https://github.com/lightpanda-io/zig-v8-fork/pull/207
Inspired by https://github.com/lightpanda-io/browser/pull/3504 this simplifies
v8::Value serialization (e.g. as used in console.log(...)).
1 - It doesn't executes JS and thus can't have a side effect, which is otherwise
possible if we invoke a getter or through a proxy
2 - It removes the debugValue debug-only path
(2) is potentially a loss in debug builds, but I think the usefulness of that
was always, at best. The upside is code elimination and consistency in how
values are reported in debug/release
setProperty, removeProperty and cssFloat rewrote the style attribute
unconditionally, so assigning a property its current value, or removing
one that was never set, produced an attribute mutation record. Chromium
emits none in those cases.
A storefront extension reacts to attribute mutations by rerendering and
reapplying styles. These spurious records keep that cycle running until
the watchdog terminates the page.
Compare the normalized value and priority before rewriting, and treat
removing an absent property as a no-op. Explicit cssText and
setAttribute assignments still notify.
Test mutation counts, priority-only changes, raw attribute preservation,
observer convergence, and healthy batches exceeding 1600 callbacks.
ReleaseFast faults otherwise leave only an exit status. Record the signal,
original fault registers and build identity without entering panic reporting,
the IO backend, an allocator-backed unwinder or telemetry.
For pipe stderr, prepare an independent nonblocking procfs descriptor before
threads start; never change the inherited descriptor's shared flags. For
sockets, use per-call nonblocking send flags. Drop output for unsupported
sinks or backpressure, then re-raise the original signal. Other platforms
retain their existing signal handling.
Core limits still apply, but cores capture the re-raise context rather than
the original fault; document that distinction. Subprocess tests cover full
pipes and sockets with undrained readers, unavailable/read-only/file stderr,
unchanged flags, repeated attachment, held panic locks and hardware faults.
Track the active-parser-was-aborted flag independently of load state. Navigation can move readyState to complete while the original parser is still on the stack; open/write/close must not start a second parser and trip ScriptManagerBase.staticScriptsDone.
Cover inline navigation, post-parse navigation cancellation, and writes after cancelling an aborted parser. Validated with 1531 passing tests on macOS arm64 and Chromium comparisons for the inline and cancellation cases.
Assisted-By: devx/f397c207-eb48-428c-a6c5-f94d95fd8ae4
A script that navigates away during parsing (a locale redirect, say)
left the old document to finish loading normally: DOMContentLoaded and
load fired on it, and clients waiting on those signals were told the
page was ready just as it was being replaced. Chrome fires none of
them: the document still transitions readyState to "complete", but
DOMContentLoaded and load never come.
Mark the document's load aborted when a cross-document navigation is
scheduled (or started directly), and keep it that way through queue
consumption and a discarded or failed replacement. document.open()
cancels the queued navigation, as in Chrome, and the rewritten document
does not get the aborted one's load back.
Tests cover the six trigger points against Chrome's event sequences,
pending and discarded replacements, open()-during-navigation, and a
readystatechange handler that renavigates and throws.
Creating the matching release on lightpanda-python fails with HTTP 403:
the distribution app's token can start workflows there but cannot write
repository contents, and granting contents:write would widen the app on
every repo it is installed on. Dispatch that repo's wheels workflow
instead — the same permission the homebrew and docker jobs use — and let
it record its own release after the PyPI publish is approved.
A multipart form POST followed by a 302 changed to GET and lost its body,
but retained Content-Type: multipart/form-data. Servers could then try to
parse an absent multipart body and return 400. This was reproduced on a
local redirect server and a storefront localization flow.
Delete Fetch's request-body header names when rewriting to GET. Preserve
method and body on 307/308, rewrite only POST on 301/302, and preserve GET
and HEAD on 303 rather than rewriting every request indiscriminately.
Test method/header transitions and header handling through the existing
CDP fulfilled-redirect path.
ScrollResult's container arm carried the node the caller had passed in,
so both consumers re-derived what they already held. The payload is now
just the scroller, the CDP handler folds two arms into one, and the tool
formats the element from its own argument instead of re-registering it.
Element.scrollContainer returns on the first hit for callers positioning
one scroller; scrollContainers keeps the per-axis walk for wheel. That
drops ScrollTargets.nearest, which only one caller read and which made
the walk keep climbing for an axis that caller discarded. ScrollTarget
gains scrollBy, so the wheel path no longer needs a free function to
pick between an element and the window.
StyleManager folds declarations through the existing declaration
iterator rather than the linked list, so Property.fromNodeLink goes back
to private. The overflow shorthand re-enters Slots.apply with its
longhand names instead of a comptime slot lookup, and both fold loops
are inline so the property name matching folds away.
The listener-list walk moves to EventManagerBase next to getListeners,
where it also skips removed listeners like findListener does. The
propagation walk takes a comptime type, so the lookup key is built once
instead of per node.
WebDriver's wheel passes the action's own coordinates through rather
than synthesizing them from a bounding rect, and its touch dispatch uses
the owner frame it already resolved.
Element.scrollContainers walks the ancestor chain once for every
requested axis and returns a ScrollTarget per axis, viewport or
container, plus the nearest of the two. The wheel path no longer walks
twice on a diagonal wheel, and the tool's "no container means the node
itself" policy is a visible switch arm rather than an orelse on null.
actions.ScrollResult names what scrolled as a tagged union: the window,
the given node, or its container together with the node. The tool and
LP.scrollNode format the result without touching the request.
CDP/BiDi wheel and WebDriver wheel each had their own definition of what
a wheel does. WebDriver computed cancelability from listener passivity
and fired Blink's legacy mousewheel; the CDP path fired a single always
cancelable wheel. Only the target lookup was legitimately different.
user_input.wheel now owns the sequence: wheel, mousewheel, then the
scroll unless either was canceled, each non-cancelable when every
listener on its path is passive. The passivity query moves onto
EventManager, next to the listeners it inspects. Callers supply the
target and the deltas.
For CDP this means a page's document-level wheel listener can no longer
cancel scrolling unless it opts out of the default-passive behavior, and
mousewheel listeners now fire, both as in Chrome.
Element.scrollContainer read the inline style= attribute only, so a
scroller declared in a stylesheet was invisible to the scroll tool and to
wheel scrolling, which then fell through to the viewport.
StyleManager now tracks overflow-x and overflow-y alongside display,
visibility, opacity and pointer-events, and exposes scrolls(el, axes) as
an own-element probe. The overflow shorthand is expanded into its
longhands in declaration order, in both the attribute scan and the
materialized style object, so a shorthand and its longhands keep the
precedence of the source text. overlay counts as auto, as in Chrome.
Element.scrollContainer asks the style manager, and the two unused Props
bits hold the new flags, so the per-element memo does not grow.
The cascade tracks display, visibility, opacity and pointer-events, and
is about to track overflow too. "Visibility" no longer describes what the
rule and property types hold.
`name` is a substring, which cannot say "starts with", "exactly this"
or "either of these". A name written as `/.../`, the spelling adblock
lists already use, is a JavaScript-syntax pattern compiled through the
App's PCRE2 context; an invalid one comes back as a tool error carrying
PCRE2's message and offset so the model can fix it rather than retry
blind.
Compiled patterns are useful anywhere someone else writes the pattern:
the adblock lists today, agent tool arguments next. The wrapper moves
out of the adblock directory and gains an options struct (case, UTF-8
subjects) and a compile diagnostic the caller can log or show. The App
owns the one context every consumer compiles through, the blocker
included.
Follow up to https://github.com/lightpanda-io/browser/pull/3510
Moves the element/node lookups, e.g. `element_class_lists` from Frame to Page.
Elements and nodes can outlive a Frame (it's the reason the identity map lives
on the Page, not the frame). These maps are merely properties on Node/Elements
optimized for a specific usage-pattern (i.e. most Node/Elements don't have these
or they are never materialized from JS). So if a Node/Element can outlive the
Frame, than so too can all of their properties. And, even when an frame is alive
the properties belong to the *Node* or *Element*, NOT the Frame...accessing
those properties across frames should yield the same value / identity.
More mechanically, frame._page => frame.page and all of these lookups lose their
_ prefix. Short summary of _ prefix is:
1 - It's used to deal with Zig not allowing shadowing. This is particularly true
in the WebApis were it happens a bit more often
2 - Early prototype was built as a stand-alone library, and the _ was used to
signal "private" (again, working around Zig). Frame.page shouldn't be
"private" and neither should these lookups (if we aren't going to provide
getter/setters for them).
Cloning reused the synchronous createElement construction path, which
rejects a result with a parent, attributes, or children. A reparenting
constructor left its instance in the source tree while the clone received
an HTMLUnknownElement fallback with different identity.
Queue an upgrade reaction for autonomous clones instead. Their copied
attributes and descendants are present when construction runs, and super()
returns the copied node. A failed upgrade retains that node rather than
substituting a second element. Keep synchronous createElement validation.
Capture initial upgrade reactions before construction and distinguish the
precustomized state so constructor-time DOM mutations do not enqueue
custom-element lifecycle reactions prematurely.
Add Chromium-checked regressions for identity, reparenting, copied state,
attribute reaction order, importNode and failed upgrades.
A custom element constructor can append itself to the source parent
while that parent's children are being cloned. With two trailing
self-appending elements, the live iterator reaches those new instances
and keeps cloning indefinitely, until the watchdog terminates execution.
Snapshot each child list before traversing it. Apply the same handling
to element, shadow root, fragment and document clone paths, retaining
the document path's existing appendChild behavior.
Add a bounded two-sibling regression so a broken traversal fails
without hanging the test process.
The scroll tool (MCP, agent, LP.scrollNode) wrote scrollTop on the exact
node it was given, so a leaf inside an overflow:auto panel stored an
offset on a non-scroller, the panel's own scroll listener never ran, and
the tool reported the requested coordinates as if it had worked. It also
fired a synchronous bubbling scroll on top of the async non-bubbling
scroll/scrollend the setters already schedule.
actions.scroll now resolves the nearest ancestor-or-self scroll
container, falls back to the node itself, and returns the node that
moved plus the read-back position. The tool and LP.scrollNode report
that instead of the request.
The container query moves from user_input.zig onto Element as
scrollContainer(axes), so the wheel path, the tool and WebDriver share
one resolver. WebDriver's wheel scrolled the hit-test element directly
and fired its own bubbling scroll; it now goes through
user_input.wheelScroll like CDP and BiDi wheel.
Window and Element share one ScrollToOpts. Its offsets() helper
normalizes the positional and dictionary forms once, and an omitted axis
in the dictionary form leaves that axis untouched for the window too,
matching browsers, so scrolling the window on one axis no longer resets
the other.
Browsers treat http://localhost, *.localhost, 127.0.0.0/8 and [::1] as
potentially trustworthy, so Secure and prefixed cookies work there over
plain http. We required an https scheme, which broke cookie-auth logins
under Playwright in local development.
Add URL.isPotentiallyTrustworthy (Chromium's net::IsLocalhost rule) and
use it for the cookie prefix gates, the send-path check and cookieStore.
Closes#3477