Commit Graph

4711 Commits

Author SHA1 Message Date
Karl Seguin
f921869fb6 Defer window.close()
Cannot Frame.deinit in window.close() as that's happening inside JS runtime.
Instead, defer on Page.deinit. This is MUCH later than necessary, but I'd like
to address the timing separately. This commit, as-is, prevents real crashes.
2026-04-30 18:04:56 +08:00
Karl Seguin
fe77694437 Merge pull request #2310 from navidemad/fix-a22-iscontenteditable
dom: implement HTMLElement.isContentEditable IDL attribute
2026-04-30 16:09:56 +08:00
Karl Seguin
a591956dcc Split Script
This is a follow up to https://github.com/lightpanda-io/browser/pull/2290. That
PR spit ScriptManager into ScriptManager and ScriptManagerBase, with the
goal of letting a Worker have a ScriptManagerBase that works without needing
a frame.

But the Script object was still a large object meant to work with any type of
script invocation, e.g. it has a nullable element. This commit swaps out these
fields for a better-typed "extra" union field. So that Element only exists in
the extra.frame tag. In the end, it just makes it more explicit about what
fields are valid under specific conditions.
2026-04-30 15:30:37 +08:00
Navid EMAD
2af95af678 dom: return false from isContentEditable, log when spec says true
Per maintainer review (PR #2310), Lightpanda has no caret/keyboard editing
pipeline, so honoring the spec-correct value risks routing downstream CDP
clients (notably Puppeteer's dispatchKeyEvent path) into an input pipeline
that silently no-ops. Switch to always returning false and emit
log.info(.not_implemented, "IsContentEditable", .{}) when the spec walk
would have produced true, so the unsupported case surfaces in telemetry
rather than masquerading as a working state.

The HTML §7.7.5.2 walk is preserved (nearest ancestor with `contenteditable`
wins, "false" disables) but only used to gate the log emission. The fixture
is reduced to assert the always-false return across the same shape of
inputs, with a comment pointing back at the rationale.
2026-04-30 07:28:43 +02:00
Patrick Wyatt
47d96ab8ad Display actual port when binding --port 0
This change causes lightpanda to display the actual port number (instead of 0)
when binding a dynamic port (--port 0), which makes automating based on
scraping lightpanda output simple.
2026-04-29 21:44:41 -07:00
Karl Seguin
e42acc5335 Merge pull request #2322 from navidemad/fix-a27-prompt-default-text
cdp: fall back to dialog defaultText when LP.handleJavaScriptDialog promptText is null
2026-04-30 12:12:33 +08:00
Karl Seguin
2c52e9b34c Merge pull request #2324 from navidemad/fix-a28-label-click-activation
dom: run label activation behavior on click
2026-04-30 11:56:59 +08:00
Karl Seguin
dd4a46dc16 Merge pull request #2331 from sunguru98/bugfix/element-getelements-by-tagname
browser: change node filter from lower to tag_name
2026-04-30 11:56:29 +08:00
Karl Seguin
64fbd55746 Merge pull request #2327 from lightpanda-io/nikneym/httponly-cookie
`Cookie`: don't allow JS context to mutate HttpOnly cookies
2026-04-30 11:52:44 +08:00
Sundeep Charan Ramkumar
91c0edddad browser: change node filter from lower to tag_name 2026-04-30 08:28:18 +05:30
Karl Seguin
1f40c30901 Merge pull request #2290 from lightpanda-io/worker-script-manager-split
Split ScriptManager to support worker module imports
2026-04-30 10:27:54 +08:00
Karl Seguin
896adc14d0 Merge pull request #2299 from lightpanda-io/idn
Initial idn support
2026-04-30 09:30:38 +08:00
Karl Seguin
c2a1b6fd64 Split ScriptManager to support worker module imports
Extract module-loading plumbing into ScriptManagerBase so workers can
use it. Previously Context.script_manager was null for worker contexts,
which crashed on dynamic import() via the unwrap in dynamicModuleCallback.

ScriptManagerBase owns the HTTP fetch + V8 module resolution path
(preloadImport, waitForImport, getAsyncImport, resolveSpecifier, the
Script struct) and reaches per-owner fields through an Owner union.
ScriptManager wraps it for Frame and keeps the parser/DOM-bound surface
(addFromElement, parseImportmap, Frame-specific evaluate tail via
tail_hook). WorkerGlobalScope gets its own ScriptManagerBase directly.

This is similar to the work done in https://github.com/lightpanda-io/browser/pull/2093
which split EventManager into EventManager + EventManagerBase (for the same
reason).

add tests
2026-04-30 09:05:51 +08:00
Karl Seguin
ce2f6d9bdb Form Submitter should only override when it's a submit input 2026-04-30 08:19:21 +08:00
Karl Seguin
20e1aeaacb Merge pull request #2282 from lightpanda-io/input_file
Input file foundation
2026-04-30 08:09:23 +08:00
Karl Seguin
3f11e6148e Merge pull request #2296 from lightpanda-io/crypto_generateKey_errors
Improve correctness of generateKey error
2026-04-30 08:08:52 +08:00
Halil Durak
0c3d5573f0 Cookie: don't allow JS context to mutate HttpOnly cookies
Changes function signature for `Jar.add` in order to do this, not sure if we should have separate functions for that or comptime-if sufficient.
2026-04-29 17:40:48 +03:00
Navid EMAD
3b3e4e4129 Merge remote-tracking branch 'origin/main' into fix-b6-validity-api 2026-04-29 15:49:37 +02:00
Navid EMAD
31e0e7c81a dom: run label activation behavior on click
`Frame.handleClick` had no `.label` arm, so clicking an `HTMLLabelElement`
fired the click event but never dispatched the synthetic click activation
on the labeled control. Per HTML §4.10.4 "The label element", a label's
activation behavior is to run the synthetic click activation steps on the
labeled control; without it `cb.checked` stays unchanged when the click
target is a `<label>`.

Resolve the labeled control via the existing `Label.getControl(frame)`
(handles both `for=` and the wrapping-descendant case) and call `.click()`
on it, mirroring Chrome's `HTMLLabelElement::DefaultEventHandler`.

Closes #2323
2026-04-29 15:24:42 +02:00
Navid EMAD
71af170658 cdp: fall back to dialog defaultText when LP.handleJavaScriptDialog promptText is null
When a CDP client pre-arms `LP.handleJavaScriptDialog {accept: true}` with
no `promptText` and the page subsequently calls `window.prompt(message,
defaultText)`, Lightpanda discarded the dialog's `defaultText` argument
and returned `""`. Chrome's behavior is to surface `defaultText` as the
prompt's return value — that's the natural "user accepted without typing
anything" outcome per the HTML simple-dialog algorithm.

The fix is one line in `Window.zig`'s `prompt` JS binding: keep the
second argument named (`default_text` instead of `_`) and use it as the
fallback. Pre-armed `promptText` still wins when the client supplies it;
`accept = false` still returns null regardless. When neither
`promptText` nor `defaultText` is provided, the binding still returns
`""` per the CDP spec.

Adds four new assertions to the existing `cdp.lp` integration test
covering the matrix: defaultText fallback (no promptText), promptText
overrides defaultText, accept=false ignores defaultText, and the
existing no-defaultText case continues to return `""`.
2026-04-29 15:01:18 +02:00
Pierre Tachoire
6e881e0a9e Merge pull request #2320 from lightpanda-io/wp/mrdimidium/fix-crash-on-frame-deinit
Fix segfault on Frame deinit
2026-04-29 14:45:10 +02:00
Navid EMAD
d66c7aad50 Merge remote-tracking branch 'origin/main' into fix-a22-iscontenteditable 2026-04-29 14:40:12 +02:00
Nikolay Govorov
91f5988fe5 Fix segfault on Frame deinit 2026-04-29 13:33:10 +01:00
Karl Seguin
f7c3ccaf85 initial (file-free) multi-part form encoding 2026-04-29 19:19:34 +08:00
Karl Seguin
7c40d2fb98 FormData File support
Move FormData from using KeyValueList to using its own List(Entry), where
entry's value is a union over a String or File. Preparatory stuff for input
type=file support.
2026-04-29 19:19:33 +08:00
Karl Seguin
b375603258 On Window.close, remove any queued navigation 2026-04-29 18:59:35 +08:00
Karl Seguin
5de4ee166f Merge pull request #2237 from lightpanda-io/window_open
window.open
2026-04-29 18:32:58 +08:00
Karl Seguin
1262964efe Merge pull request #2308 from navidemad/fix-a26-textarea-crlf
forms: normalize CR/LF to CRLF in form-data set encoding
2026-04-29 17:07:45 +08:00
Karl Seguin
21e9c5184c Fix atob/btoa
Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/175

This started as an investigation into errors when loading recaptcha. Turns out
our atob/btoa is naive. Specifically, we take a `[]const u8` input which, via
the bridge, goes through the v8 utf8 path. This can result in the input being
fundamentally altered.

This commit introduces a new discriminatory type, js.String.OneByte which uses
the String::NewFromOneByte when going from js->zig and String:WriteOneByte when
going from zig->js, avoiding any changes to the raw data.

Also adds a placeholder MessageEvent.ports getter (which showed up in debugging)
2026-04-29 17:01:07 +08:00
Adrià Arrufat
92db1b4a5e Merge pull request #2294 from navidemad/fix-a24-ua-stylesheet-display-none
css: apply UA stylesheet display:none defaults for unrendered elements
2026-04-29 08:51:53 +02:00
Adrià Arrufat
d92ebdeb89 css: apply zig fmt to Element.zig 2026-04-29 08:22:55 +02:00
Navid EMAD
aca6518850 selector: gate :disabled / :enabled on the HTML "disabled concept"
Per HTML "concept-fe-disabled", only listed elements (button, input,
select, textarea, optgroup, option, fieldset) participate in the
disabled concept. Anything else (e.g. <div disabled>) has no disabled
state and never matches :disabled / :enabled.

Add Element.hasDisabledConcept() and gate isDisabled() on it. Update
the :enabled selector arm so non-form-controls no longer match.
2026-04-29 05:50:54 +02:00
Navid EMAD
628c170b11 Merge remote-tracking branch 'origin/main' into fix-a21-disabled-inheritance 2026-04-29 05:42:27 +02:00
Karl Seguin
78babf4017 Merge pull request #2261 from navidemad/fix-a3-handle-javascript-dialog
lp: add LP.handleJavaScriptDialog to drive confirm/prompt return values
2026-04-29 11:12:05 +08:00
Navid EMAD
7dccf59ec2 zig fmt linting 2026-04-29 03:45:09 +02:00
Navid EMAD
82497a78a8 selector: walk fieldset/optgroup ancestors when matching :disabled
The `:disabled` and `:enabled` pseudo-class matchers in
`webapi/selector/List.zig` only checked the element's own `disabled`
content attribute. Per HTML "selector-disabled" + "concept-fe-disabled"
+ "concept-option-disabled", a form control inside <fieldset disabled>
matches :disabled (with the first-<legend> exception), and an <option>
inside <optgroup disabled> matches :disabled.

Route both pseudo-classes through `Element.isDisabled` (which already
walks the fieldset chain) and extend `isDisabled` to cover the
<option> + <optgroup disabled> case. <option> intentionally does NOT
inherit from <fieldset disabled> or <select disabled> — confirmed
against Chrome.

Closes #2314
2026-04-29 03:36:41 +02:00
Karl Seguin
9d271ce4ae Fix more DOM assumptions that callback and document.write can break
As we've seen before, both document.write and custom elements callbacks can
introduce otherwise impossible states. See https://github.com/lightpanda-io/browser/pull/2172
for just one example.

The issue with both is that they can trigger code during DOM manipulation that
renders the state of that DOM manipulation invalid. This commit started with
a fairly easy case to understand (one that actually happened in production). It
goes something like:

```
<div id=x>
  <script>
    document.write('<script>x.innerHTML = "";</script>');
  </script>
</div>
```
Here we see that the written script deletes the node which is in the process
of being rendered.

These issues are almost always easy to fix, but they're hard to predict. After
fixing this issue, I asked Claude to check for other cases, and it was able to
find / reproduce / fix two more involving custom elements callback.
2026-04-29 09:09:07 +08:00
Navid EMAD
6211b21813 dom: route <input type=image> click into form submission
Frame.handleClick's .input arm matched only _input_type == .submit and
fell through for .image, so clicking an image button fired the click
event but never scheduled a navigation or dispatched the form's submit
event. Per HTML §4.10.18.6.4 image buttons are submit buttons and must
submit their owning form.

The submitForm path already passes the input element as the submitter,
and FormData.collectForm already handles image submitters by appending
`name.x` and `name.y` (or bare `x`/`y` for unnamed inputs) coordinate
entries to the form-data set. Only the routing in handleClick was
missing. Coordinates are 0 for programmatic .click() per the spec.

Closes #2311
2026-04-29 02:52:50 +02:00
Navid EMAD
5454d6a213 dom: implement HTMLElement.isContentEditable IDL attribute
`Element.isContentEditable` returned `undefined` because no IDL accessor
existed on HTMLElement. Per HTML §7.7.5.2 the IDL attribute returns
`true` iff the element's effective content editable state is "true" or
"plaintext-only". Walk up to the nearest ancestor (or self) carrying a
`contenteditable` attribute; the keyword `false` maps to the false
state, every other value (empty string, "true", "plaintext-only", or
unrecognized) maps to an editable state. With no ancestor carrying the
attribute the document's designMode default ("off") yields `false`.

Closes #2309
2026-04-29 02:32:42 +02:00
Navid EMAD
8cc82d1d64 lp: move handleJavaScriptDialog pre-arm from Page to LP namespace
Per upstream review (#2261): Page.handleJavaScriptDialog is a standard
CDP method that reactive Chrome-style drivers send in response to a
Page.javascriptDialogOpening event. Repurposing it for the proactive
pre-arm flow risks surprising those drivers — the next dialog they
trigger would be blindly accepted.

Move the pre-arm handler to LP.handleJavaScriptDialog so only
Lightpanda-aware clients opt in. Page.handleJavaScriptDialog reverts
to the upstream stub returning -32000 "No dialog is showing" so the
reactive surface is unchanged. The Page.javascriptDialogOpening event
listener still pops the BrowserContext stash and fills the response
output param — only the setter moved.

Tests rename cdp.page: → cdp.lp: and target LP.handleJavaScriptDialog;
behavior coverage is identical.
2026-04-29 02:04:20 +02:00
Navid EMAD
6c2d78d0f0 Merge remote-tracking branch 'origin/main' into fix-a3-handle-javascript-dialog 2026-04-29 01:56:55 +02:00
Navid EMAD
7c6f2a2a8c forms: normalize CR/LF to CRLF in form-data set encoding
Per the HTML form-data set encoding algorithm, every U+000A (LF) not
preceded by U+000D (CR) and every U+000D (CR) not followed by U+000A
(LF) in an entry's name or value is replaced with the two-byte sequence
CR+LF before percent-encoding. Without this pass, a textarea API value
containing LF (per the textarea wrapping transformation) submits as raw
%0A on the wire instead of the spec-required %0D%0A.

The normalization is gated on URLEncodeMode == .form so URLSearchParams
(.query) still follows the URL standard's serializer, which doesn't
normalize. Same pass covers names + values for both the POST
application/x-www-form-urlencoded body and the GET query-string path
(both go through the same encoder before URL.concatQueryString).

Closes #2307
2026-04-29 01:53:17 +02:00
Karl Seguin
e133b26e9b Merge pull request #2306 from lightpanda-io/feat/2249-htmlframesetelement-stub
Feat/2249 htmlframesetelement stub
2026-04-29 07:46:06 +08:00
Karl Seguin
f15948c5a4 Finalize *partial* frameset support 2026-04-29 07:16:33 +08:00
Navid EMAD
fd2f26a065 Merge remote-tracking branch 'origin/main' into fix-a3-handle-javascript-dialog 2026-04-29 00:57:03 +02:00
Navid EMAD
dfa0ba778d css: address review feedback on UA display:none helper
- Tighten matchesUaDisplayNoneRule docstring; lead with the
  centralization purpose and HTML Rendering §15.3.1 spec citation.
- Reorder checks: tag-name UA list first (O(1) switch, exits for
  ~95% of elements with ordinary tags before the attribute lookup).
- Use el.is(Input) + input._input_type == .hidden instead of
  case-insensitive string compare on the raw "type" attribute;
  matches the pattern used in EventManager / Form / RadioNodeList,
  and _input_type is parsed case-insensitively at attribute-set time.
- Compress the comment block above the matchesUaDisplayNoneRule
  call site in isElementHidden.

Behavior-preserving refactor; full unit test suite (494 tests) passes.
2026-04-29 00:53:10 +02:00
Navid EMAD
7c190d9e69 Merge remote-tracking branch 'origin/main' into fix-a24-ua-stylesheet-display-none 2026-04-29 00:47:56 +02:00
Muki Kiboigo
e8c9acd310 fix request arena leak on CacheLayer hit 2026-04-28 09:48:23 -07:00
guyua9
55a1efc7ed fix: accept CDP SameSite cookie casing 2026-04-29 00:44:08 +08:00
Karl Seguin
9fe628dd0f Initial idn support
Links to libidn2 and builds libcurl with it. This makes libcurl work, and by
extension browser, work on international domain names, e.g.

zig build run -- fetch "https://räksmörgås.se/"

With it available, we can use it in our WebAPIs which should also support these
domains, e.g:
  testing.expectEqual('xn--rksmrgs-5wao1o.se', new URL('https://räksmörgås.se').hostname);

There is more integration to be done here, but this is a first step.

claude wrote all of the build.zig code.

I don't have a strong opinion about this feature, I just dislike that our WPT
/url/* tests are at 1704 / 9095 and, this is the biggest chunk (although, this
specific commit just does the basic integration and probably won't fix too many
WPT cases directly).
2026-04-28 22:21:27 +08:00