Commit Graph
7731 Commits
Author SHA1 Message Date
Adrià Arrufat 7b30a7447e input: activate buttons on Enter's keypress, submit once
Chrome clicks a button on the keypress Enter produces, after the keypress
fires; only links follow Enter on the keydown. Clicking on the keydown put
the click before the keypress and, with the char step also submitting for
button-type inputs, submitted the form twice.

The MCP press action relied on the same char step for Enter, so its own
implicit submission is gone with the double submit it caused on buttons.
2026-09-17 12:45:49 +02:00
Adrià Arrufat 74bf43e732 input: type text in a char step, not in the keydown default action
Follow Chrome's model. A keydown only runs its own default action (Tab,
caret moves, Backspace, Enter activation). Text is typed by the char half
of the press, which fires keypress and then beforeinput/textInput, so
either can veto the edit.

A keyDown carrying `text` runs the char step inline (Puppeteer,
Playwright). A text-less keyDown followed by a `char` message runs it on
the char (chromedp), so each character is typed once. A cancelled
text-less keyDown drops the char that follows it, as Chrome does.

BiDi, WebDriver and the MCP press action go through the same pressKey,
deriving the text from the key. pressKey holds a ref on the keydown for
the keypress it builds, so nothing reads the event after dispatch.
Input.insertText fires beforeinput like a key press does.
2026-09-17 09:53:47 +02:00
Adrià Arrufat 681a3e75d3 test(cdp): split and simplify char and keyDown input tests 2026-09-16 18:55:09 +02:00
Adrià Arrufat fb95e8b39e cdp: window.devicePixelRatio from viewport scale and Input char text insertion
- window.devicePixelRatio: turn from a constant property into an accessor
  mirroring innerWidth/innerHeight. Reads the page viewport scale (set via
  Emulation.setDeviceMetricsOverride's deviceScaleFactor, default 1.0) and
  remains [Replaceable] through a setter that delegates to replaceGlobalProperty.
- Input.dispatchKeyEvent:
  - For `char` events, insert `params.text` into the focused element when
    not default-prevented by a keypress listener. Non-text keys without a `text`
    payload (e.g. Enter) leave the value untouched.
  - For `keyDown` events, fall back to `params.text` when `params.key` is
    omitted, allowing text-only keyDown dispatches from CDP clients to insert
    the character into the focused element.
2026-09-16 18:52:20 +02:00
Karl Seguin 2648b3067c Merge pull request #3539 from lightpanda-io/indexeddb-double-free
crash: fix double free in indexeddb
2026-09-16 20:23:13 +08:00
Karl Seguin afbc8378b1 Merge pull request #3522 from lightpanda-io/regex-shared
`Regex`: share the PCRE2 wrapper; `findElement` matches names by `/regex/`
2026-09-16 16:55:40 +08:00
Karl Seguin 828130fdcd Merge pull request #3530 from staylor/fix/superseded-document-lifecycle
Stop load events on a document superseded by navigation
2026-09-16 15:46:01 +08:00
Karl Seguin 482ea04292 crash: fix double free in indexeddb
Add state to OpenContext so that the owner is tracked known and other flows
don't free (cancelPark doesn't free when the context is running, since run will
take care of it)
2026-09-16 15:42:09 +08:00
Adrià Arrufat 90487270ba findElement: the name filter is one union; a literal is one by grammar
A name is a substring or a regex, never both, so the filter says so
instead of carrying two optionals the caller has to null against each
other. Whether `/.../x` is a literal is now decided by JavaScript's flag
alphabet rather than "looks like letters", which stops `/usr/bin` from
being read as a pattern with flags. Comments that narrated callers or
the type name are gone; the literal parser gets its own test in place
of two MCP round-trips.
2026-09-16 09:38:42 +02:00
Adrià Arrufat 5710f1d684 findElement: accept the flags of a JavaScript regex literal
A model that writes `/spice/i` should not be told the name has a
stray `i` in it. `i` and `u` are already how names match, `s` and `m`
map to their PCRE2 options, and any other letter is a tool error that
names it. Text after the closing slash that is not a letter makes the
whole thing a plain substring again.
2026-09-16 09:29:18 +02:00
Adrià Arrufat 35dbda3686 Regex: compile through the context
The context is what a pattern is compiled against, so `compile` reads
better as its method than as a free function taking it first.
2026-09-16 09:28:45 +02:00
Karl Seguin 21a210fe16 Narrow frame suspension
The original work was aimed at ensuring that a page which is being navigated
away doesn't trigger events that it shouldn't, i.e. `DOMContentLoaded` and
`load`.

This just narrows down the scope of that little, for example ensuring the new
page actually gets its navigate() off being signaling the old page to abort.
2026-09-16 14:08:11 +08:00
Karl Seguin d52957b2e2 Merge pull request #3533 from staylor/fix/fatal-signal-backtrace
Record Linux fatal signals without blocking on stderr
2026-09-16 13:59:29 +08:00
Karl Seguin b3b663ce43 fix ci 2026-09-16 13:10:52 +08:00
Karl Seguin 7b7c7d7b0b Expand to destination to all stderr destination
Attempt to add backtrace information

If there's no PATH, look for curl in known locations. (this is unrelated, but
there's overlap with this work and the special `segv-crash-report` that we're
using in debugging, and i wanted to bring this specific part over).
2026-09-16 11:47:40 +08:00
Karl Seguin ea0443cec9 internal: simplify v8.Value serialization
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
2026-09-16 10:12:27 +08:00
Karl Seguin 6d87fbf0e9 Merge pull request #3526 from staylor/investigate/localization-400
Drop body headers when redirects switch to GET
2026-09-16 09:06:40 +08:00
Scott Taylor 45eb267f5c cssom: only sync the style attribute when a declaration changes
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.
2026-09-16 07:02:54 +08:00
Karl Seguin 7571d6e7a0 Merge pull request #3527 from lightpanda-io/nikneym/stdin-js-exec
`Agent`: add `lightpanda run -`
2026-09-16 06:40:41 +08:00
Karl Seguin f5c1772edb Merge pull request #3516 from lightpanda-io/readonly-computedStyles
webapi: computedStyle are now readonly
2026-09-16 06:38:49 +08:00
Scott Taylor 02a0a33498 Record Linux fatal signals without blocking on stderr
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.
2026-09-15 15:30:11 -04:00
Pierre Tachoire 9bf65fac5a add missing LLM providers in help 2026-09-15 18:34:24 +02:00
Scott Taylor 39dde6db7c document: keep an aborted parser from restarting after navigation
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
2026-09-15 12:23:13 -04:00
Pierre Tachoire c1ddf920ec document --dump pdf in help 2026-09-15 18:13:33 +02:00
Pierre Tachoire 580658676e remove the removed --storage-* from help 2026-09-15 18:11:30 +02:00
Scott Taylor 1a9557e41e frame: stop a superseded document's load events once navigation begins
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.
2026-09-15 11:31:09 -04:00
Halil Durak 901e5eaf4d Agent: add lightpanda run -
Arbitrary JS code can be passed directly through stdin thanks to this.
2026-09-15 16:21:01 +03:00
Scott Taylor 8339361043 http: drop request-body headers when redirects rewrite the method
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.
2026-09-15 07:48:18 -04:00
Karl Seguin 5f73e07cd6 Merge pull request #3520 from lightpanda-io/page-lookups
webapi: better frame integrity
2026-09-15 17:21:43 +08:00
Adrià Arrufat 158de60d64 findElement: match accessible names by regex
`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.
2026-09-15 09:33:39 +02:00
Adrià Arrufat d2f72a9fa9 Regex: share the PCRE2 wrapper beyond adblock
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.
2026-09-15 09:33:39 +02: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 ee015da56c webapi: computedStyle are now readonly
getComputedStyle returns a CSSStyleDeclaration that should be readonly. We now
return a NoModificationAllowed on write.

Fixes exactly 1 WPT case =)
2026-09-15 09:56:25 +08: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