- Cache `ValidityState` per element so repeated `el.validity` access
returns the same instance (also avoids allocating a struct per
per-keystroke call).
- Use `Event.initTrusted` for the `invalid` event — browser-generated.
- Use `frame.dupeString(message)` (interns short strings) instead of
`frame.arena.dupe(u8, message)` for `setCustomValidity` storage.
- Cascade `*const Input` / `*const Select` through `getValidationMessage`,
`suffersValueMissing`, and Select.effectiveOption.
- Factor radio-group walking into a shared `radioGroupIterator`; add the
same-form check to `radioGroupHasChecked` (was only in
`uncheckRadioGroup`). The form comparison uses a frame-less helper so
it can run from the const validation path.
- Narrow `numericRangeBreach` to `.number, .range` — date/time/month/
week/datetime-local need type-specific conversion before comparison
(was silently returning false). TODO comment left in place.
- Add `getMinLength`/`setMinLength` getters to Input and TextArea, plus
`getMaxLength`/`setMaxLength` to TextArea (Input already had them).
Used internally by `suffersTooLong`/`suffersTooShort` and exposed via
IDL.
- TODO prefix on the file-input value-missing comment so the limitation
is greppable.
Tests cover the new IDL surface (`minLength`/`maxLength` on Input and
TextArea), the validity-state caching guarantee
(`el.validity === el.validity`), and the trusted-event flag on the
`invalid` event for Input, Select, TextArea, and Button.
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.
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.
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
`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
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 `""`.
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.
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.
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
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
`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
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.
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
- 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.
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).