Commit Graph

6690 Commits

Author SHA1 Message Date
Navid EMAD
d1a0203d88 make: forward optional V8_PATH to zig build
Without -Dprebuilt_v8_path, the build/test targets rebuild V8 from
source (10+ minutes per invocation). Contributors who already have a
cached archive can now short-circuit by exporting V8_PATH:

    V8_PATH=v8/libc_v8.a mise exec -- make test

When V8_PATH is empty (default), behavior is unchanged.
2026-05-15 22:53:00 +02:00
Navid EMAD
ee1cbf1bb3 make: add clean target 2026-05-15 22:51:54 +02:00
Navid EMAD
b9601be45e accessibility: bind AX writers to the node's owning frame
axnodeWriter and axnodeQueryWriter both used session.currentFrame(),
but the root node may belong to a different frame (cross-frame query
from a parent context, iframe content). Name resolution (Label lookup
against ownerDocument) and visibility checks (frame._style_manager)
are per-frame, so the writer needs to bind to the node's owning frame.

Uses the existing Node.ownerFrame(fallback) helper. Fallback is
currentFrame for the orphan/detached-node case.

Also corrects a pre-existing latent bug in getFullAXTree where the
writer ignored the resolved frameId and used currentFrame instead.
2026-05-15 22:26:48 +02:00
Navid EMAD
940e9e9240 accessibility: promote hidden-input labels in queryAXTree
Match Writer.writeNode (line 488-499): when a <label> targets a CSS-hidden
checkbox/radio (toggle-switch / CSS-only radio pattern), emit the label
with the input's role so clients searching by role land on the label's
clickable backendDOMNodeId rather than the hidden input.

Without this, queryAXTree(role="checkbox", name=...) on a toggle-switch
returns the display:none input — a non-actionable target.
2026-05-15 22:18:55 +02:00
Navid EMAD
5f2d897f16 accessibility: implement queryAXTree CDP method
Adds Accessibility.queryAXTree for finding AX nodes by role and/or
accessible name without serializing the full tree. Motivated by
agentic / MCP automation workloads where getFullAXTree round-trips
multi-MB JSON on complex pages.

QueryWriter walks the DOM subtree rooted at the requested node,
reuses the existing role + name resolution + ignore logic from
AXNode, and emits a flat array of matches. Reuses the same
VisibilityCache + LabelByForIndex + temp arena pattern as
axnodeWriter, so no extra retained state.

MVP limitations, each a small follow-up PR:
- objectId param returns a specific not-yet-supported error
- matches emit empty properties/childIds and no parentId
- frameId not parsed; queries the current frame
2026-05-15 21:38:17 +02:00
Navid EMAD
dd5e335262 css: harden media-query evaluator and @media boundary
Address review feedback on PR #2478:

- MediaQuery.zig: strip CSS `/* ... */` comments before tokenization so
  `screen and /*x*/ (min-width: 1px)` evaluates the same as without the
  comment.
- MediaQuery.zig: bound-check `em` / `rem` multiplication via
  `std.math.mul` so a u32-overflowing length (e.g. `268435456em`) fails
  closed instead of panicking in debug or wrapping in release.
- StyleManager.zig: prelude brace search skips `/* ... */` comments, so
  `@media /* { fake */ screen { ... }` splits at the real opening brace
  rather than the one inside the comment.
- Tests: unit tests for stripped comments, em/rem overflow, and
  unimplemented units (cm/mm/pt/in/vw). HTML fixtures cover commented
  preludes/queries and the `replaceSync` cascade path.
2026-05-15 20:49:45 +02:00
Navid EMAD
68bd1441af css: evaluate @media and matchMedia against viewport
Inline `@media` rules were parsed but never applied to the cascade, and
`window.matchMedia(q).matches` always returned false. Add a Media Queries
Level 4 subset evaluator (`width`/`height`/`orientation`, lengths in
`px`/`em`/`rem`, comma OR, `and`, `not`, `only`) wired into both surfaces.
External `<link rel="stylesheet">` fetch remains out of scope; the
evaluator reads the same 1920x1080 viewport already exposed by
`Window.innerWidth` / `innerHeight`.

Closes #2477
2026-05-15 20:30:01 +02:00
Navid EMAD
353be6382d js: emit null when JSON-stringifying unserializable values
V8's `JSON::Stringify` finishes by calling `Object::ToString` on whatever
`i::JsonStringify` returns. For values that `JSON.stringify` treats as
non-serializable at the top level (`undefined`, functions, symbols),
`i::JsonStringify` yields the undefined sentinel and `ToString` coerces
it to the JS string `"undefined"`. `Value.jsonStringify` then wrote those
9 bytes raw via `writer.writeAll`, embedding a bare `undefined` token in
the JSON stream — invalid per RFC 8259 and rejected by any strict-JSON
CDP client. Detect the sentinel and emit JSON `null` instead, matching
what `JSON.stringify` produces when the same value sits in an array slot
(`JSON.stringify([undefined])` → `"[null]"`).

Closes #2473
2026-05-15 19:21:57 +02:00
Scott Taylor
4bd2edb596 Fix #2472: scope frame ID generator to Browser, not Session
CDP target IDs (`FID-{d:0>10}`) must stay unique for the lifetime of
the CDP connection -- Playwright's `CRBrowser._onAttachedToTarget`
asserts on duplicates and the assertion is fatal (the connection is
unusable afterwards).

Before this fix, `Session.frame_id_gen` reset to 0 in two places:

  1. `tearDownActivePage` explicitly reset to 0 after every page
     teardown (likely intended to mimic pre-pending-page numbering
     within a single Session, but invisible there because the
     immediately-following `installNewActivePage` typically reuses
     the old frame's explicit `frame_id`, see `replaceRootImmediate`).

  2. Fresh Sessions started from the field default of 0. Each
     `Target.createBrowserContext` calls `Browser.newSession`, which
     deinits the old Session and constructs a new one -- so even
     without (1), the next BrowserContext's first page would still
     get `FID-0000000001`.

(2) is what trips Playwright on the second `browser.newContext()`
on a connection: the second context's first frame re-issues
`FID-0000000001`, identical to the first context's frame, and
Playwright's `CRBrowser._onAttachedToTarget` raises
`Duplicate target FID-0000000001`.

Move `frame_id_gen` (and `nextFrameId`) from `Session` to `Browser`,
which is per-CDP-connection. Existing callers (`Session.createPage`,
`Frame.zig:1327`, `Frame.zig:1437`, `Worker.zig:74`) still go through
`Session.nextFrameId` -- it's now a thin pass-through to
`browser.nextFrameId()` -- so no call sites change. Removed the
explicit reset in `tearDownActivePage`; it was redundant within a
Session (root navigation reuses the old frame_id) and harmful across
Sessions.

`loader_id_gen` stays on Session: Loader IDs (`LID-...`) are scoped
per-frame in CDP and Playwright doesn't track them in the target
registry, so the per-Session reset is correct there.

Repro (`playwright-core@1.58.2`):

  for (let i = 1; i <= 3; i++) {
    const ctx = await browser.newContext();
    await ctx.newPage();
    await ctx.close();
  }

Before: cycle 2 throws `Duplicate target FID-0000000001`.
After: 5/5 cycles complete cleanly.

Tests: 653/653 pass. Added regression coverage in
`cdp.target: createTarget assigns unique IDs across BrowserContexts
(issue #2472)` -- verified to fail against the original source
(reverted Browser.zig and Session.zig, kept the test, ran zig build
test: only the new test fails).
2026-05-15 13:17:49 -04:00
Halil Durak
34557e3993 cli.zig: update doc comment 2026-05-15 18:31:10 +03:00
Halil Durak
658df6e500 cli.zig: support lightpanda help <command>
Another variation to receive `help` text for a specific command.
2026-05-15 18:31:10 +03:00
Halil Durak
3489129f68 main.zig: changes for new help 2026-05-15 18:31:10 +03:00
Halil Durak
b2d8c2b834 help.zon: introduce help.zon
Separates `help` explanation from configuration.
2026-05-15 18:31:09 +03:00
Halil Durak
f361f12316 cli.zig: change the way help command and sub-command detected
`cli.zig` is now aware of `help` command at all situations and creates it by itself. Instead of using errors, it initializes `Command` union where `help` branch is active.
2026-05-15 18:31:09 +03:00
Halil Durak
c993ba48a9 cli.zig: rewrite doc comment 2026-05-15 18:31:09 +03:00
Halil Durak
9c40cd9fb2 send Accept header when navigating 2026-05-15 18:30:18 +03:00
Pierre Tachoire
4f33d64c5c Merge pull request #2433 from lightpanda-io/webmcp
Implement webMCP API and webMCP cdp domain
2026-05-15 16:13:12 +02:00
Pierre Tachoire
60e3d48dbd webmcp: update comments 2026-05-15 15:12:27 +02:00
Pierre Tachoire
f00c0ab276 webmcp: implement abortSignal with _dependent 2026-05-15 13:11:59 +02:00
Pierre Tachoire
3803a1f8c6 webmcp: use value.jsonStringify for JSON write 2026-05-15 11:17:53 +02:00
Pierre Tachoire
dbb9b31061 webmcp: fix invoke callback with correct ModelContextClient param 2026-05-15 11:00:56 +02:00
Karl Seguin
64c5843e9e Merge pull request #2466 from lightpanda-io/pending_queue_pump
Clear pending destroy on createPage (a known safepoint).
2026-05-15 16:42:15 +08:00
Pierre Tachoire
7c5a3b211f cdp: cancel inflight webmcp invocation on bc deinit 2026-05-15 08:50:48 +02:00
Pierre Tachoire
19fd9a6e35 cdp: adjust inv_id address usage 2026-05-15 08:50:47 +02:00
Pierre Tachoire
5e0901aaf7 cdp: fix invalid arena usage in webmcp 2026-05-15 08:50:47 +02:00
Pierre Tachoire
3ef6e57d58 cdp: adjust invocation id usage for webmcp 2026-05-15 08:50:47 +02:00
Pierre Tachoire
c23d0f4f35 cdp: implement webMCP domain 2026-05-15 08:50:46 +02:00
Pierre Tachoire
0023bd7d19 Add WebMCP navigator.modelContext
Implements the page-side surface of the W3C WebMCP spec
(https://webmachinelearning.github.io/webmcp/): exposes
`navigator.modelContext.registerTool(...)` for declaring MCP tools to a
browser agent, with full name/description validation, AbortSignal-based
unregistration, and a `ModelContextClient` whose `requestUserInteraction`
invokes its callback directly (closest faithful behavior in a headless
browser).
2026-05-15 08:50:38 +02:00
Karl Seguin
a5162bea8f Cleanup HttpClient.Transfer
This is just moving fields around. The end result is that there's a
`transfer.req` and a `transfer.res`.

On the Request side, we use to have a nested `params: RequestParam` resulting
in a lot of `transfer.req.params.url`. This is now `transfer.req.url`. On the
Response side, we had the exact opposite: response fields splattered directly
in the transfer, `transfer.response_header`. This is now `transfer.res.header`.

There is now an HttpClient.Response, which is the actual final response (which
could be for a transfer or something else, e.g the cache). And an
HttpClient.Transfer.Response which captures the inflight response data (and is
one of the polymorphic variants of the HttpClient.Response). Probably still not
ideal, but I'm not sure how to make it cleaner, and even if this is just an
intermediary step, I consider it an small win.
2026-05-15 12:55:47 +08:00
Karl Seguin
4205cd905b Clear pending destroy on createPage (a known safepoint).
This allows pending destroys that have been accumulated to be cleaned up. In
normal operations, this likely isn't going to happen. But we see a some unit
tests create _many_ pages that never have the change to be cleaned up. The
result is that the next "normal" unit test, which actually runs enough through
Runner to trigger the cleanup, pays a huge cleanup price.

Arguably, for a test-only solution, we could create a session per test, or
have explicit cleanup in the test. But having 1 long-lasting session is useful
as it can show us these potential pitfalls AND, it isn't impossible that a
real-world case runs into similar issues.
2026-05-15 11:31:53 +08:00
Karl Seguin
cb8c2bc4d8 Merge pull request #2456 from lightpanda-io/cdp-proper-cache-disable
properly disable cache on `Network.setCacheDisabled`
2026-05-15 09:11:12 +08:00
Karl Seguin
632f3ea7d6 Merge pull request #2457 from lightpanda-io/fetch_dump_navigate_fix
Dump using the latest Frame to prevent segfault during on frame change
2026-05-15 07:38:19 +08:00
Karl Seguin
94f0d94192 Merge pull request #2461 from staylor/fix/2459-surface-at-rules-via-insertrule
Surface at-rules through insertRule and replaceSync (fixes #2459 partial)
2026-05-15 07:35:59 +08:00
Karl Seguin
b7a0ca2bca fallback unknown rule to new unknown type 2026-05-15 06:59:21 +08:00
Scott Taylor
6d1740b40f Surface at-rules through insertRule and replaceSync (fixes #2459)
CSSStyleSheet.insertRule previously detected at-rules in the parser
(has_skipped_at_rule) and returned the requested index without inserting
anything. The original change (PR #1972) did this to keep at-rule input
from killing module evaluation in apps like Expo Web -- correct, but the
silent-success contract has a second-order effect: CSS-in-JS libraries
(emotion, styled-components, Stitches, Mantine, Linaria) that round-trip
through cssRules to deduplicate their stylesheets see the rule as missing
after insertion, conclude the stylesheet is empty, and fall back to direct
<style> element injection per render. The result is unbounded <style>
accumulation on long-lived sessions doing repeated DOM interaction. See
issue #2459 for measurements (Allbirds-style PDPs accumulating thousands
of <style> elements over a render loop).

Changes:

* Parser.RulesIterator now returns a Rule union of {.style, .at_rule}
  instead of skipping at-rules and setting has_skipped_at_rule. The
  at-rule variant carries the keyword (without `@`) and the full source
  span so callers can construct an opaque placeholder rule.

* CSSRule gains a `_text` field and an `initAtRule` constructor for
  storing the at-rule source. CSSRule.getCssText returns the stored
  text (CSSStyleRule's overridden getCssText still wins for `.style`
  rules via the bridge dispatch on the most-derived class).

* CSSStyleSheet.insertRule and replaceSync handle both Rule variants:
  regular rules become CSSStyleRule as before, at-rules become opaque
  CSSRule placeholders with the matching CSSRule.Type variant
  (vendor-prefixed keyframes are normalized; unrecognized at-rule
  keywords fall back to .media). The CSS engine still doesn't apply
  these rules -- that's intentional and outside the scope of this
  change -- but they now surface via cssRules so library dedup paths
  work correctly.

* StyleManager.addRawRules switches on the Rule kind and skips
  at-rules (it only filters on display/visibility/opacity at the top
  level, no semantic change).

* The CSSRule spec constants (STYLE_RULE, KEYFRAMES_RULE, MEDIA_RULE,
  ...) were declared as plain Zig consts inside JsApi but never wrapped
  with bridge.property, so they came back as `undefined` from JS. Fixed
  while writing the regression tests since the tests need to compare
  rule.type against the spec constants.

Tests:

* Parser unit tests: cover statement at-rules (`@import url(...);`),
  block at-rules (`@media`), and vendor-prefixed at-rules
  (`@-webkit-keyframes`).

* HTML runner tests: cover insertRule for @keyframes, @media,
  @supports, @font-face, vendor-prefixed at-rules, mixed style + at-rule
  insertion, replaceSync at-rule preservation, and the dedup-via-cssRules
  pattern that's the actual library code path the bug breaks.

A/B verification with the synthetic CSS-in-JS dedup pattern (50 calls to
inject the same @keyframes through a library that falls back to <style>
element injection when insertRule appears empty):

  baseline (1.0.0-nightly.6240): styles=51 rules=0
  patched (this branch):         styles=1  rules=50

The leak collapses: instead of 50 <style> fallbacks stacking up, the
single persistent stylesheet receives all 50 insertions and dedup works.

Note on partial coverage of #2459: the original Allbirds reproducer
involves a Vue.js + Yotpo widget that injects <style> elements via
direct document.head.appendChild rather than insertRule. That code
path is unaffected by this change; it appears to be a separate
mechanism (possibly related to Vue's vue-style-loader closure-based
dedup or web-component lifecycle on Lightpanda) and is worth filing
separately. This PR fixes the specific insertRule contract issue
described in #2459 and unblocks the major CSS-in-JS libraries.
2026-05-14 16:19:36 -04:00
Muki Kiboigo
940976b6a7 properly disable cache on Network.setCacheDisabled 2026-05-14 09:03:51 -07:00
Karl Seguin
a59ddeb360 Dump using the latest Frame to prevent segfault during on frame change
Fixes: https://github.com/lightpanda-io/browser/issues/2446
2026-05-14 20:00:20 +08:00
Karl Seguin
2f3a426fb0 Merge pull request #2453 from lightpanda-io/cdp-network-serve-from-cache
Adds `Network.requestServedFromCache`
2026-05-14 17:34:46 +08:00
Karl Seguin
b96c24d377 Merge pull request #2455 from lightpanda-io/cdp-response-fromdiskcache
Add `fromDiskCache` field to `Network.Response`
2026-05-14 16:01:04 +08:00
Karl Seguin
0624a05205 Merge pull request #2454 from lightpanda-io/cdp-network-cache-clear
add `Network.clearBrowserCache` and `Network.canClearBrowserCache`
2026-05-14 15:56:29 +08:00
Karl Seguin
143bffdfec Merge pull request #2450 from navidemad/fix-bug7-form-idl
forms: add enctype + 5 submitter form-* IDL accessors
2026-05-14 13:44:57 +08:00
Karl Seguin
80a09fc0fd zig fmt 2026-05-14 13:19:17 +08:00
Muki Kiboigo
f2f328cffd add fromDiskCache field to Network.Response CDP type 2026-05-13 21:57:22 -07:00
Muki Kiboigo
07e7c3d687 add Network.clearBrowserCache and Network.canClearBrowserCache 2026-05-13 21:52:10 -07:00
Muki Kiboigo
ac863c7e2b add Network.requestServedFromCache 2026-05-13 21:47:47 -07:00
Karl Seguin
14b4449628 use format to write String value 2026-05-14 11:03:12 +08:00
Karl Seguin
373916873f Merge pull request #2442 from lightpanda-io/worker_message_buffer
CI fixes, callback timing correctness
2026-05-14 08:56:36 +08:00
Karl Seguin
96ac9a49ea Update src/browser/webapi/Worker.zig
Co-authored-by: Navid EMAD <navid.emad@yespark.fr>
2026-05-14 08:33:32 +08:00
Karl Seguin
1580ab197f Merge pull request #2452 from lightpanda-io/event_worker
make Event worker-safe
2026-05-14 07:39:37 +08:00
Karl Seguin
bcafa175cb make Event worker-safe 2026-05-14 07:11:33 +08:00