Commit Graph
7700 Commits
Author SHA1 Message Date
Karl Seguin 5f73e07cd6 Merge pull request #3520 from lightpanda-io/page-lookups
webapi: better frame integrity
2026-09-15 17:21:43 +08:00
Karl Seguin 1163096946 handle constructor that disconnect itself 2026-09-15 13:43:16 +08:00
Karl Seguin 48dc72cd49 webapi: better frame integrity
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).
2026-09-15 11:26:04 +08:00
Scott Taylor c0ae2ea137 dom: upgrade autonomous custom-element clones in place
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.
2026-09-14 22:30:25 -04:00
Scott Taylor 5ab9f69b44 dom: clone a snapshot of children, not the live list
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.
2026-09-14 22:13:56 -04:00
Karl Seguin baf45f03e9 Merge pull request #3515 from lightpanda-io/scroll-tool-container
Scroll the nearest scroll container from the scroll tool
2026-09-15 08:58:15 +08:00
Karl Seguin 8bc4cf6877 Merge pull request #3514 from lightpanda-io/loopback-secure-cookies
cookies: accept Secure and __Host- cookies on loopback origins
2026-09-15 07:54:53 +08:00
Adrià Arrufat 1e67c6fc91 Scroll the nearest scroll container from the scroll tool
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.
2026-09-14 17:48:25 +02:00
Adrià Arrufat 1ede559ff9 cookies: accept Secure and __Host- cookies on loopback origins
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
2026-09-14 16:02:26 +02:00
Halil Durak 93381010f1 Merge branch 'main' into nikneym/lax-exception-RFC6265bis 2026-09-14 14:27:55 +03:00
Karl Seguin 72a05c7efa Merge pull request #3510 from lightpanda-io/node-document
internal: store document in every node
2026-09-14 17:41:55 +08:00
Karl Seguin fbbfae2d29 assert node has a non-null document 2026-09-14 17:23:13 +08:00
Karl Seguin eb82e71403 Merge pull request #3511 from lightpanda-io/parser-chunked-microtasks
internal: run microtasks during parsing
2026-09-14 16:05:10 +08:00
Karl Seguin fa2e21cb57 Minor cleanup
?*Element -> *Element to getAttribute, getOrCreateAttribute, and toAttribute
which never need the option.

Assert that document._page == page.

Text use asCData() instead of constantly calling Factory.protoOf.
2026-09-14 14:53:16 +08:00
Karl Seguin 5068f6c811 Merge pull request #3509 from lightpanda-io/css-class-name
webapi: (minor) CSS WebApi should be called "CSS" not "Css"
2026-09-14 14:35:37 +08:00
Karl Seguin 2927799cd3 Merge pull request #3508 from lightpanda-io/improve-js-memory-metric
ops: improve v8 memory metric reporting
2026-09-14 14:35:12 +08:00
Karl Seguin 7382efe688 Merge pull request #3501 from lightpanda-io/frameless-documents
webapi: better handling of frameless documents
2026-09-14 14:35:00 +08:00
Karl Seguin 729051b279 internal: run microtasks during parsing
Runs microtasks during parsing, rather than waiting for parsing to complete.

We see some sites that setup MutationObserver on the root, early in a document's
code. These then get thousands (4K-10K) of MutationRecords for every added node.
We deliver these as a single batch at the end of parsing. Chrome delivers it
based on some elapsed time calculation (I think).

Both are correct, as far as I can tell. And, I don't really expect this to
change anything. But, because the batch size is fixed:

1 - the inflight metric should be flatter
2 - there could be some small reduction in retained memory (e.g. MO's
    `_pending_records` might not grow so much)
3 - `call_arena` doesn't need to dupe such a large array

Related to (1), when we do special builds that log arena usage and eliminating /
reducing this known OK behavior helps remove some noise (Or, put it this way:
I spent some time debugging this, it's more or less nothing, but it still
looks like something that needs fixing, this commit reduces that noise).
2026-09-14 13:38:05 +08:00
Karl Seguin ef18e0e1cd internal: store document in every node
Follow up to https://github.com/lightpanda-io/browser/pull/3501.

To recap: many apis use the injected-frame, when, for cross-frame operations,
can be wrong. We've been moving to using the Node's Document's Frame on a case-
by-case basis. The goal with this series of PRs is to more holistically fix it.

The main goal of this commit is to make Node.getDocument fast. Previously,
Node.nodeDocument (renamed to Node.getDocument) and Node.ownerFrame had to walk
up the _parent tree to find the document. Now there's a DocumentRegistry on
the Browser, and every Node has a `_document: u32` index. Thus, Node -> Document
is O(1). This makes it so we can have correctness without a performance cost.

The DocumentRegistry is held on the Browser because of shared CDP nodes. A
follow up should be able to move this to the Page and change the index to u16.

Adding a DocumentRegistry to Browser, and a document: u32 to Document is easy.
The reason this PR is so big is because every `node_factory` and many Factory
methods need to be aware of all of this. You can't create an HTMLDivElement
without the document it belongs to. I incorporated more expected future changes
into the node_factory/factory mechanism so that  [hopefully] the next PRs
don't have to touch any of this code (e.g. I removed Frame as a parameter and
added a *Page to every Document so that documents have access to factory/arena/
etc..because a `*Document` might not have a `*Frame`, but it will always have a
`*Page`.

Give every document an index (u32), and store that index in Node.
2026-09-14 12:05:52 +08:00
Karl Seguin 5163c6d385 Merge pull request #3506 from lightpanda-io/silence-test-logs
chore: silence expected error log in MO test
2026-09-14 08:57:19 +08:00
Karl Seguin ea1ce09753 webapi: (minor) CSS WebApi should be called "CSS" not "Css" 2026-09-14 08:56:24 +08:00
Karl Seguin 19b165d099 Merge pull request #3502 from lightpanda-io/selected-options
chore: cleanup selected / selected options
2026-09-14 08:19:34 +08:00
Karl Seguin 3adacb6900 Merge pull request #3505 from staylor/fix/window-tostringtag
Fix Symbol.toStringTag inheritance for globals and elements
2026-09-14 08:19:09 +08:00
Karl Seguin f2abadbb0e Merge pull request #3484 from lightpanda-io/xhr_cors
XHR changes for CORS WPT tests
2026-09-14 08:18:44 +08:00
Karl Seguin dcd5c796ed ops: improve v8 memory metric reporting
Report the v8 memory every 1 second during tick. The previous model only
reported at four points and would fail to correct capture memory growth between
those points.
2026-09-14 08:15:40 +08:00
Karl Seguin 27b16af770 zig fmt *eyeroll* 2026-09-14 08:05:12 +08:00
Karl Seguin adb2b21ccf chore: silence expected error log in MO test 2026-09-14 08:01:00 +08:00
Muki Kiboigo 22e095370b add unit test for XMLHttpRequest parseMethod 2026-09-14 07:58:57 +08:00
Muki Kiboigo 00c98313c2 use curl no body option for head requests 2026-09-14 07:58:56 +08:00
Muki Kiboigo b3655f5918 shortcut default/text getResponse instead of caching path 2026-09-14 07:58:07 +08:00
Muki Kiboigo 8bbe1907b0 parseMethod for HEAD requests in XHR 2026-09-14 07:58:07 +08:00
Muki Kiboigo 3a7b3f9e1f XHR response can return before done on text/default 2026-09-14 07:58:07 +08:00
Karl Seguin 47eb1b8581 make acquireArena names more consistent with each other 2026-09-14 07:44:21 +08:00
Scott Taylor f8183e2168 js: inherit Symbol.toStringTag from interface prototypes
Web IDL defines Symbol.toStringTag on interface prototype objects, not
instances. Setting it on instance templates left globals and internal
custom/generic element subclasses without a tag, so they reported
[object Object]. Interface prototypes also reported the wrong class string.

Deep-clone helpers that recurse into plain objects consequently traverse
host objects and their cyclic references. Locally, a storefront theme's
store cloner classified custom/generic elements as plain objects and
exhausted the JS heap, terminating the CDP connection.

Set the tag on the member template: interface prototypes for inheritance,
while namespace objects keep their existing own-property behavior.
Test class strings, property descriptors, namespace ownership, and bounded
deep cloning that preserves host-object identity.
2026-09-13 19:27:18 -04:00
Scott Taylor c284fce11a cdp: preserve message storage across reentrant notifications
Runtime.consoleAPICalled serialized object arguments with JSON.stringify,
which can run getters and toJSON callbacks. A callback can log again or
navigate, emitting further CDP events while the outer event is being built.
Resetting send_arena after each send and notification_arena after each
handler then invalidates the outer message's buffers. This produces
use-after-free in debug builds or malformed JSON that disconnects clients.

Scope both arenas to the outermost send or notification handler, including
inspector messages and error paths. Also match Chrome's console argument
representation: objects remain remote handles, primitives carry value,
and non-JSON numbers and bigints use unserializableValue. Console logging
must not invoke object getters or toJSON as a serialization side effect.

Test complete nested WebSocket messages, failure recovery, nested legacy
Console notifications, and primitive/object protocol shapes.
2026-09-13 19:27:18 -04:00
Adrià Arrufat db9779a654 Enable CURLOPT_PIPEWAIT on every easy handle
Requests issued to an origin while its first connection is still
handshaking each opened their own socket, up to --http-max-host-open,
because curl only learns from ALPN whether the origin multiplexes. With
pipewait they wait for that answer and share one h2 connection.

Fixture: 12 fetch() calls to a fresh cdnjs (h2) origin, release build.

  new TCP+TLS connections   6 -> 1
  in-page time to last resp ~285 ms -> ~105-135 ms

H1-only origins are unchanged in connection count; their first burst
waits one handshake before fanning out.
2026-09-13 22:09:36 +02:00
Karl Seguin 0d9e5441f5 chore: cleanup selected / selected options
There's been recent work on improve select / options:
- https://github.com/lightpanda-io/browser/pull/3375
- https://github.com/lightpanda-io/browser/pull/3402
- https://github.com/lightpanda-io/browser/pull/3499

One of the main issues is that a select's options "selected" state wasn't always
kept in sync. Select.zig had a boolean flag to mark whether or not a
selectedIndex was explicit set and every read and update would need to dance
around it. Removing the selectedIndex option would not, for example, keep things
in sync.

This removes the flag and keeps the Option._selected in sync, i.e. the sync
happens on write, not on read and is thus naturally recorded in the state of
the Select and its Options.

The write path is more complicated, but the read path is simpler (though the
real win is always being correct).
2026-09-13 14:13:33 +08:00
Scott Taylor 6d454529a5 Fix select value assignment with no matching option
An unmatched select.value assignment must leave every option unselected,
with value == "" and selectedIndex == -1. Reapplying the first-option
fallback on reads instead can keep change-driven select mirroring loops
from converging.

Preserve an explicitly cleared selection until the option list changes.
Restore the single-select default after insertion, removal, moves, and
fragment parsing, without inventing a selection for multiple selects or
listboxes. Select only the first matching option for value assignments.

Cover empty values, duplicate values, grouped options, structural resets,
and bounded select mirroring; compare the new assertions with Chromium.
2026-09-12 23:58:50 -04:00
Scott Taylor 02c4ef8777 test: exception object relocated by GC while its TryCatch is read
Regression page for eff118eb2 (don't copy TryCatch by value): a
MutationObserver callback throws an object whose `message` getter
allocates enough to force several scavenges while `TryCatch.caught` is
between `Exception()` and `StackTrace()`. On a copy of the TryCatch the
relocated exception's old address was read back; with a debug V8 this
failed 7/7 runs before that fix (unknown instance type, or a fault inside
the pointer-compression cage).
2026-09-12 22:32:21 -04:00
Karl Seguin d1ea326b44 webapi: better handling of frameless documents
We've auto-injected `*Frame` into WebApi since forever (used to be called *Page,
but then we split *Page / *Frame, but same same). And it worked wonderfully:
there was always a single *Frame, so the Frame/Context that the JS was being
executed in HAD to be the *Frame that a node belonged to.

But with the addition of iframe and popups, that truth no longer holds. The
*Frame executing the JS (which is the frame that we auto-inject) isn't
necessarily the *Frame that owns a Node.

This is particularly problematic because the *Frame holds a bunch of node/
element data, e.g. `_element_datasets`. So now the DataSet that you get back
depends on the context in which its called..they don't have identity and can
fall out of sync. Some code calls node.ownerFrame() / node.ownerDocument(), but
not all and the hope is to address this throughout the codebase once and for
all.

This is the first in a series of commits meant to fix this long-standing
issue. All it does is change the node.ownerFrame() return value from *Frame to
?*Frame. It's up to each caller to decide how to handle a frameless node, e.g.
clicking a frameless link should not navigate.
2026-09-13 10:31:30 +08:00
Karl Seguin 799dc3e06d Merge pull request #3479 from lightpanda-io/wheel-scrolls-viewport
cdp: wheel events scroll the viewport unless over a scroll container
2026-09-13 09:06:29 +08:00
Karl Seguin cc64255b35 Merge pull request #3497 from lightpanda-io/try-catch-copy
crash: don't copy TryCatch by value
2026-09-13 09:06:06 +08:00
Karl Seguin 9b61b498a9 make css property compare case-insensitive 2026-09-13 08:41:31 +08:00
Karl Seguin ee735143b2 Merge pull request #3474 from lightpanda-io/prune-hidden-walks
Probe own visibility inside walks that prune hidden subtrees
2026-09-13 08:30:26 +08:00
Karl Seguin 128233c4ff Merge pull request #3494 from lightpanda-io/robot-store-leak
mem: fix robot store leak
2026-09-13 08:29:51 +08:00
Karl Seguin eff118eb2e crash: don't copy TryCatch by value
The v8.Handle is initialized / registered with v8 at that address and can't
be copied.
2026-09-13 08:23:57 +08:00
Adrià Arrufat 91cdb52829 cdp: prune accessibility tree children when node is hidden
💘 Generated with Crush

Assisted-by: Crush:gemini-3.8-flash
2026-09-12 14:28:22 +02:00
Adrià Arrufat 55dcad50d1 cdp: wheel scrolls per axis and lets scrollBy schedule the scroll events
Each wheel axis now looks for the nearest ancestor whose inline overflow
along that axis is auto or scroll, and scrolls it through Element.scrollBy,
or the viewport through Window.scrollBy, so the trusted scroll and
scrollend events are scheduled once instead of firing a second, bubbling
scroll inline. The lookup reads the inline style straight from the style
manager instead of building a computed-style object per ancestor.

Both scrollBy implementations saturate the addition, so an oversized delta
from CDP or from a script no longer overflows the i32 position.
2026-09-12 14:12:27 +02:00
Adrià Arrufat 365bc1e1d2 Merge remote-tracking branch 'origin/main' into wheel-scrolls-viewport 2026-09-12 14:07:06 +02:00
Karl Seguin e8aa759390 Merge pull request #3495 from lightpanda-io/HttpClient-Owner-scope
chore: dedupe HttpClient.Owner using new GlobalScope
2026-09-12 16:33:17 +08:00