capybara-lightpanda 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/lib/capybara/lightpanda/browser.rb +34 -3
- data/lib/capybara/lightpanda/javascripts/polyfills.js +165 -34
- data/lib/capybara/lightpanda/node.rb +31 -13
- data/lib/capybara/lightpanda/process.rb +7 -5
- data/lib/capybara/lightpanda/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cb36e68fb7bcc35abcbae5a3417091eb54444f7b57e161cb44cb6436d243684
|
|
4
|
+
data.tar.gz: 9eff2c5cd278d16ed8ad55d106dca14dab3597a87c3315c46371b98672a35163
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 156126b7f57785869aca23b79fe02bc3fe7370cf5b2826999715d5e69794a6b62f8c69bb64601e5e67f935d946e2348274c8ec76d79b674930b239d6b2383677
|
|
7
|
+
data.tar.gz: c73dab9284d0d8e0091794e2f88674951bafc64af2fa2e87200926a60d7d86767f31cd5114d5348af51013fe3b4846f1066d0bdca886a5a1051432673b76ef52
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2] - 2026-05-06
|
|
4
|
+
|
|
5
|
+
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6065 (published 2026-05-06). The driver refuses to start against older binaries.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Turbo Drive `<form>` submissions now intercept correctly. Forms inside a
|
|
10
|
+
Hotwire / Turbo Drive page no longer crash on submit, and Turbo's `submit`
|
|
11
|
+
interceptors fire as they should — `click_button`, `find('form').submit`,
|
|
12
|
+
and Enter-key implicit submission all complete end-to-end.
|
|
13
|
+
- `evaluate_script` and `execute_script` calls with top-level `const` / `let`
|
|
14
|
+
no longer collide across calls. Consecutive scripts that each declare the
|
|
15
|
+
same identifier used to fail with `Identifier 'foo' has already been
|
|
16
|
+
declared`; they're now isolated. `execute_script` also now raises on
|
|
17
|
+
JavaScript errors instead of silently swallowing them.
|
|
18
|
+
- Same-document fragment-only `<a href="#…">` clicks update the URL hash
|
|
19
|
+
instead of triggering a real navigation. Tests that drive DOM updates from
|
|
20
|
+
an anchor click no longer lose pending `setTimeout` callbacks or have form
|
|
21
|
+
values cleared from under them.
|
|
22
|
+
- `body` returns an empty string rather than crashing during the brief window
|
|
23
|
+
after `reset_session!` when the new session has a target but no document yet.
|
|
24
|
+
- Stale element references during cross-document navigation now resolve to
|
|
25
|
+
`nil` internally instead of bubbling a browser error up to your test,
|
|
26
|
+
letting Capybara's automatic-reload pick a fresh element.
|
|
27
|
+
|
|
28
|
+
### Internal
|
|
29
|
+
|
|
30
|
+
- One internal polyfill removed: Lightpanda now matches the spec when a DOM
|
|
31
|
+
event listener throws (a throwing listener no longer halts the rest of the
|
|
32
|
+
bubble walk), so the gem doesn't need to compensate. No code change required
|
|
33
|
+
on your end.
|
|
34
|
+
|
|
3
35
|
## [0.2.1] - 2026-05-05
|
|
4
36
|
|
|
5
37
|
### Fixed
|
|
@@ -238,7 +238,11 @@ module Capybara
|
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
def body
|
|
241
|
-
|
|
241
|
+
# Guard against the brief window after a fresh BrowserContext / target
|
|
242
|
+
# is created where the V8 context exists but `document.documentElement`
|
|
243
|
+
# is still null. Hit by Capybara's `#reset_session! resets page body`
|
|
244
|
+
# spec since the 0.2.0 Ferrum-style reset rewrite.
|
|
245
|
+
evaluate("(document.documentElement && document.documentElement.outerHTML) || ''")
|
|
242
246
|
end
|
|
243
247
|
alias html body
|
|
244
248
|
|
|
@@ -247,9 +251,25 @@ module Capybara
|
|
|
247
251
|
# and dispatch via Runtime.callFunctionOn so `arguments[i]` is bound.
|
|
248
252
|
# Both paths use `returnByValue: false` and unwrap so DOM-node returns
|
|
249
253
|
# come back as `{ "__lightpanda_node__" => ... }` for the Driver to wrap.
|
|
254
|
+
#
|
|
255
|
+
# Even the no-args path wraps the expression in an IIFE to isolate
|
|
256
|
+
# top-level `const`/`let` declarations. Upstream Lightpanda retains
|
|
257
|
+
# those bindings across `Runtime.evaluate` calls (V8 starts each call
|
|
258
|
+
# with fresh lexical scope per spec), so a second `const sel = ...`
|
|
259
|
+
# raises `SyntaxError: Identifier 'sel' has already been declared`.
|
|
260
|
+
# Wrapping pushes the declarations into a function scope that gets
|
|
261
|
+
# discarded when the IIFE returns.
|
|
262
|
+
#
|
|
263
|
+
# Use direct `eval` inside the IIFE so the user's text can be a bare
|
|
264
|
+
# expression (`'foo'`), a `throw` statement, OR a multi-statement
|
|
265
|
+
# script with `const`/`let`. `eval`'s completion-value semantics
|
|
266
|
+
# return the last expression's value in all cases. A naive
|
|
267
|
+
# `return EXPR;` wrap would syntax-error on `throw …` and on
|
|
268
|
+
# multi-statement scripts.
|
|
250
269
|
def evaluate(expression, *args)
|
|
251
270
|
if args.empty?
|
|
252
|
-
|
|
271
|
+
wrapped = "(function(){return eval(#{expression.to_json})})()"
|
|
272
|
+
response = page_command("Runtime.evaluate", expression: wrapped, returnByValue: false, awaitPromise: true)
|
|
253
273
|
if response["exceptionDetails"]
|
|
254
274
|
debug_js_failure("evaluate", expression, response)
|
|
255
275
|
raise JavaScriptError, response
|
|
@@ -263,9 +283,20 @@ module Capybara
|
|
|
263
283
|
end
|
|
264
284
|
|
|
265
285
|
# Execute JS without returning a value.
|
|
286
|
+
#
|
|
287
|
+
# Like `evaluate`, the no-args path wraps in an IIFE — same upstream
|
|
288
|
+
# `const`/`let` leak. Also raises on JS exceptions so silent
|
|
289
|
+
# failures don't mask test bugs (the previous fast path swallowed them
|
|
290
|
+
# because `awaitPromise: false` was checked but `exceptionDetails` was
|
|
291
|
+
# not).
|
|
266
292
|
def execute(expression, *args)
|
|
267
293
|
if args.empty?
|
|
268
|
-
|
|
294
|
+
wrapped = "(function(){#{expression}})()"
|
|
295
|
+
response = page_command("Runtime.evaluate", expression: wrapped, returnByValue: false, awaitPromise: false)
|
|
296
|
+
if response["exceptionDetails"]
|
|
297
|
+
debug_js_failure("execute", expression, response)
|
|
298
|
+
raise JavaScriptError, response
|
|
299
|
+
end
|
|
269
300
|
return nil
|
|
270
301
|
end
|
|
271
302
|
|
|
@@ -5,6 +5,78 @@
|
|
|
5
5
|
(function () {
|
|
6
6
|
"use strict";
|
|
7
7
|
|
|
8
|
+
// ── Bug #7 — HTMLFormElement / HTMLButtonElement / HTMLInputElement form-* IDL gaps ──
|
|
9
|
+
// Lightpanda doesn't expose `form.enctype`, `form.method`, `form.action`,
|
|
10
|
+
// `form.target`, nor the submitter-side `formEnctype` / `formMethod` /
|
|
11
|
+
// `formAction` / `formTarget` overrides. Per WHATWG HTML these must always
|
|
12
|
+
// return a string (with the spec's missing-value default) so consumers can
|
|
13
|
+
// call `.toLowerCase()` etc. directly. Turbo's `FormSubmission` constructor
|
|
14
|
+
// does exactly that and crashes with `Cannot read properties of undefined
|
|
15
|
+
// (reading 'toLowerCase')` when it touches enctype.
|
|
16
|
+
//
|
|
17
|
+
// Polyfill strategy: only define the IDL getter when it's missing on the
|
|
18
|
+
// prototype, so a future Lightpanda nightly that adds native support wins
|
|
19
|
+
// automatically. Each getter falls back to the underlying attribute, with
|
|
20
|
+
// the spec's default if the attribute is absent. For submitter overrides
|
|
21
|
+
// (formEnctype, formMethod, etc.) we return the empty string when the
|
|
22
|
+
// override attribute is unset — Turbo and Hotwire all use the
|
|
23
|
+
// `submitter.formX || form.X` idiom, which resolves correctly when the
|
|
24
|
+
// submitter side returns "".
|
|
25
|
+
(function patchFormIDL() {
|
|
26
|
+
var ENCTYPE_VALUES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
|
|
27
|
+
function normEnctype(v) {
|
|
28
|
+
if (!v) return "application/x-www-form-urlencoded";
|
|
29
|
+
v = String(v).toLowerCase();
|
|
30
|
+
return ENCTYPE_VALUES.indexOf(v) >= 0 ? v : "application/x-www-form-urlencoded";
|
|
31
|
+
}
|
|
32
|
+
function normMethod(v) {
|
|
33
|
+
if (!v) return "get";
|
|
34
|
+
v = String(v).toLowerCase();
|
|
35
|
+
return (v === "post" || v === "dialog") ? v : "get";
|
|
36
|
+
}
|
|
37
|
+
function defineIfMissing(proto, name, getter) {
|
|
38
|
+
if (!proto || name in proto) return;
|
|
39
|
+
try { Object.defineProperty(proto, name, { configurable: true, enumerable: true, get: getter }); } catch (_) {}
|
|
40
|
+
}
|
|
41
|
+
if (typeof HTMLFormElement !== "undefined") {
|
|
42
|
+
var fp = HTMLFormElement.prototype;
|
|
43
|
+
defineIfMissing(fp, "enctype", function () { return normEnctype(this.getAttribute("enctype")); });
|
|
44
|
+
defineIfMissing(fp, "method", function () { return normMethod(this.getAttribute("method")); });
|
|
45
|
+
defineIfMissing(fp, "action", function () {
|
|
46
|
+
var a = this.getAttribute("action");
|
|
47
|
+
if (a == null || a === "") return (this.ownerDocument && this.ownerDocument.URL) || "";
|
|
48
|
+
try { return new URL(a, (this.ownerDocument && this.ownerDocument.URL) || undefined).href; }
|
|
49
|
+
catch (_) { return a; }
|
|
50
|
+
});
|
|
51
|
+
defineIfMissing(fp, "target", function () { return this.getAttribute("target") || ""; });
|
|
52
|
+
}
|
|
53
|
+
function patchSubmitter(Ctor) {
|
|
54
|
+
if (typeof Ctor === "undefined") return;
|
|
55
|
+
var p = Ctor.prototype;
|
|
56
|
+
// Empty string is the spec's missing-value default for the submitter-side
|
|
57
|
+
// IDL attrs — keep Turbo's `submitter.formX || form.X` idiom flowing
|
|
58
|
+
// through to the form's value.
|
|
59
|
+
defineIfMissing(p, "formEnctype", function () {
|
|
60
|
+
var v = this.getAttribute("formenctype");
|
|
61
|
+
return v == null ? "" : normEnctype(v);
|
|
62
|
+
});
|
|
63
|
+
defineIfMissing(p, "formMethod", function () {
|
|
64
|
+
var v = this.getAttribute("formmethod");
|
|
65
|
+
return v == null ? "" : normMethod(v);
|
|
66
|
+
});
|
|
67
|
+
defineIfMissing(p, "formAction", function () {
|
|
68
|
+
var a = this.getAttribute("formaction");
|
|
69
|
+
if (a == null || a === "") return "";
|
|
70
|
+
try { return new URL(a, (this.ownerDocument && this.ownerDocument.URL) || undefined).href; }
|
|
71
|
+
catch (_) { return a; }
|
|
72
|
+
});
|
|
73
|
+
defineIfMissing(p, "formTarget", function () { return this.getAttribute("formtarget") || ""; });
|
|
74
|
+
defineIfMissing(p, "formNoValidate", function () { return this.hasAttribute("formnovalidate"); });
|
|
75
|
+
}
|
|
76
|
+
patchSubmitter(typeof HTMLButtonElement !== "undefined" ? HTMLButtonElement : null);
|
|
77
|
+
patchSubmitter(typeof HTMLInputElement !== "undefined" ? HTMLInputElement : null);
|
|
78
|
+
})();
|
|
79
|
+
|
|
8
80
|
// ── Bug #4 — HTMLDialogElement.{showModal, show, close} non implémentés ──
|
|
9
81
|
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element
|
|
10
82
|
if (typeof HTMLDialogElement !== "undefined") {
|
|
@@ -35,47 +107,106 @@
|
|
|
35
107
|
}
|
|
36
108
|
}
|
|
37
109
|
|
|
38
|
-
// ── Bug #
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// phase
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
110
|
+
// ── Bug #8 (added 2026-05-05) — sync remove + re-add lost across dispatch phases ──
|
|
111
|
+
// WHATWG DOM specifies that each phase of a dispatch snapshots `currentTarget`'s
|
|
112
|
+
// listener list AT THAT PHASE. Listeners removed and re-added during the capture
|
|
113
|
+
// phase correctly appear in the bubble-phase snapshot in Chrome/Cuprite. Lightpanda
|
|
114
|
+
// takes the snapshot once at dispatch start, so a remove+add during capture loses
|
|
115
|
+
// the listener for the in-flight bubble. This breaks Turbo's `FormSubmitObserver`
|
|
116
|
+
// pattern, where `submitCaptured` does `remove+add` on its own `submitBubbled` to
|
|
117
|
+
// ensure that handler runs LAST in bubble — under Lightpanda, `submitBubbled` is
|
|
118
|
+
// dropped entirely and Turbo never intercepts form submissions.
|
|
119
|
+
//
|
|
120
|
+
// Native form submission via `requestSubmit()` doesn't route through JS-exposed
|
|
121
|
+
// `dispatchEvent`, so we can't detect "in-flight dispatch" by patching that. The
|
|
122
|
+
// workaround instead targets the remove+add idiom directly: defer every
|
|
123
|
+
// `removeEventListener` to a microtask. When `addEventListener` runs in the same
|
|
124
|
+
// synchronous turn with the SAME (target, type, fn, capture), we cancel the
|
|
125
|
+
// pending remove — the listener was never actually unregistered, so the in-flight
|
|
126
|
+
// bubble snapshot still contains it. Genuine removes (no matching add follows)
|
|
127
|
+
// happen at end-of-tick, indistinguishable from the unpatched behavior modulo
|
|
128
|
+
// tick boundary.
|
|
129
|
+
//
|
|
130
|
+
// Scope: only capture-phase `submit` listeners, the exact tuple Turbo Drive
|
|
131
|
+
// uses. This avoids changing observable DOM semantics for arbitrary
|
|
132
|
+
// remove/dispatch sequences elsewhere on the page — synchronous remove +
|
|
133
|
+
// dispatch outside this narrow tuple still fires the native `removeEventListener`
|
|
134
|
+
// path immediately.
|
|
135
|
+
//
|
|
136
|
+
// Trade-offs (within the narrowed scope):
|
|
137
|
+
// • Code that removes a capture-phase submit listener and then reads
|
|
138
|
+
// listener state synchronously before the microtask flush will see it
|
|
139
|
+
// as still-attached. No known framework does this on `submit`.
|
|
140
|
+
// • If something removes capture-phase submit listener X then adds Y of
|
|
141
|
+
// the same type/capture before the flush, the add happens immediately
|
|
142
|
+
// but the deferred remove fires after, removing X *after* Y is
|
|
143
|
+
// registered. Y persists, X is gone. Same end state as without the
|
|
144
|
+
// polyfill, just reordered in time.
|
|
145
|
+
(function patchListenerLifecycle() {
|
|
146
|
+
if (!window.EventTarget || !EventTarget.prototype.addEventListener) return;
|
|
147
|
+
if (typeof Promise === "undefined") return; // need microtasks
|
|
148
|
+
var origAdd = EventTarget.prototype.addEventListener;
|
|
149
|
+
var origRemove = EventTarget.prototype.removeEventListener;
|
|
150
|
+
|
|
151
|
+
function captureFlag(opts) {
|
|
152
|
+
return opts === true || (opts && typeof opts === "object" && opts.capture === true);
|
|
153
|
+
}
|
|
48
154
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} catch (err) {
|
|
53
|
-
if (!event || !event.bubbles || !this.parentNode) throw err;
|
|
155
|
+
function inScope(type, capture) {
|
|
156
|
+
return type === "submit" && capture === true;
|
|
157
|
+
}
|
|
54
158
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Object.defineProperty(event, "target", {
|
|
58
|
-
value: originalTarget,
|
|
59
|
-
configurable: true,
|
|
60
|
-
});
|
|
61
|
-
} catch (_) { /* target not redefinable — continue anyway */ }
|
|
159
|
+
var pending = []; // [{ target, type, fn, capture, cancelled }]
|
|
160
|
+
var flushScheduled = false;
|
|
62
161
|
|
|
63
|
-
|
|
64
|
-
|
|
162
|
+
function scheduleFlush() {
|
|
163
|
+
if (flushScheduled) return;
|
|
164
|
+
flushScheduled = true;
|
|
165
|
+
Promise.resolve().then(function () {
|
|
166
|
+
flushScheduled = false;
|
|
167
|
+
var queue = pending;
|
|
168
|
+
pending = [];
|
|
169
|
+
for (var i = 0; i < queue.length; i++) {
|
|
170
|
+
var r = queue[i];
|
|
171
|
+
if (r.cancelled) continue;
|
|
65
172
|
try {
|
|
66
|
-
|
|
67
|
-
value: node,
|
|
68
|
-
configurable: true,
|
|
69
|
-
});
|
|
173
|
+
origRemove.call(r.target, r.type, r.fn, r.capture);
|
|
70
174
|
} catch (_) {}
|
|
71
|
-
try {
|
|
72
|
-
orig.call(node, event);
|
|
73
|
-
} catch (_) { /* ignore intermediate crashes; keep propagating */ }
|
|
74
|
-
if (event.cancelBubble) break;
|
|
75
|
-
node = node.parentNode || node.host || null;
|
|
76
175
|
}
|
|
77
|
-
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
EventTarget.prototype.removeEventListener = function (type, fn, opts) {
|
|
180
|
+
var capture = captureFlag(opts);
|
|
181
|
+
if (!fn || !inScope(type, capture)) return origRemove.call(this, type, fn, opts);
|
|
182
|
+
pending.push({
|
|
183
|
+
target: this,
|
|
184
|
+
type: type,
|
|
185
|
+
fn: fn,
|
|
186
|
+
capture: capture,
|
|
187
|
+
cancelled: false,
|
|
188
|
+
});
|
|
189
|
+
scheduleFlush();
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
EventTarget.prototype.addEventListener = function (type, fn, opts) {
|
|
193
|
+
if (!fn) return origAdd.call(this, type, fn, opts);
|
|
194
|
+
var capture = captureFlag(opts);
|
|
195
|
+
if (inScope(type, capture)) {
|
|
196
|
+
// Cancel a pending remove for the same tuple (LIFO so the most recent
|
|
197
|
+
// pending remove wins for the remove-then-add idiom). Always call
|
|
198
|
+
// native add too: addEventListener is idempotent per DOM spec, and
|
|
199
|
+
// skipping risks losing the listener on a later unmatched remove.
|
|
200
|
+
for (var i = pending.length - 1; i >= 0; i--) {
|
|
201
|
+
var r = pending[i];
|
|
202
|
+
if (r.cancelled) continue;
|
|
203
|
+
if (r.target === this && r.type === type && r.fn === fn && r.capture === capture) {
|
|
204
|
+
r.cancelled = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
78
208
|
}
|
|
209
|
+
return origAdd.call(this, type, fn, opts);
|
|
79
210
|
};
|
|
80
211
|
})();
|
|
81
212
|
})();
|
|
@@ -237,6 +237,8 @@ module Capybara
|
|
|
237
237
|
|
|
238
238
|
def backend_node_id
|
|
239
239
|
@backend_node_id ||= driver.browser.backend_node_id(@remote_object_id)
|
|
240
|
+
rescue BrowserError
|
|
241
|
+
nil
|
|
240
242
|
end
|
|
241
243
|
|
|
242
244
|
private
|
|
@@ -363,13 +365,6 @@ module Capybara
|
|
|
363
365
|
end
|
|
364
366
|
end
|
|
365
367
|
|
|
366
|
-
# Native `this.click()` reaches all ancestors on the happy path, but if any
|
|
367
|
-
# listener throws (Stimulus / Turbo edge cases) Lightpanda halts dispatch
|
|
368
|
-
# instead of reporting the exception per DOM §2.9 step 4 (see UPSTREAM_BUGS.md
|
|
369
|
-
# Bug #3). Dispatching via JS routes through `polyfills.js`'s patchDispatch
|
|
370
|
-
# IIFE, which catches the throw and re-walks parents manually so document-
|
|
371
|
-
# level delegated handlers still see the event.
|
|
372
|
-
#
|
|
373
368
|
# We dispatch a `MouseEvent` (not a generic `Event`) because Turbo's link
|
|
374
369
|
# and form interceptors guard with `event instanceof MouseEvent` before
|
|
375
370
|
# they consider intercepting — a synthetic `Event('click')` is silently
|
|
@@ -390,10 +385,7 @@ module Capybara
|
|
|
390
385
|
function() {
|
|
391
386
|
var EventCtor = (typeof MouseEvent !== 'undefined') ? MouseEvent : Event;
|
|
392
387
|
var clickEvt = new EventCtor('click', { bubbles: true, cancelable: true });
|
|
393
|
-
var notCancelled =
|
|
394
|
-
try {
|
|
395
|
-
notCancelled = this.dispatchEvent(clickEvt);
|
|
396
|
-
} catch (e) { /* patchDispatch in polyfills.js rescues bubble phase */ }
|
|
388
|
+
var notCancelled = this.dispatchEvent(clickEvt);
|
|
397
389
|
if (!notCancelled || clickEvt.defaultPrevented) return;
|
|
398
390
|
var tag = this.tagName;
|
|
399
391
|
var type = (this.type || '').toLowerCase();
|
|
@@ -401,9 +393,35 @@ module Capybara
|
|
|
401
393
|
(tag === 'BUTTON' && (type === 'submit' || type === '')) ||
|
|
402
394
|
(tag === 'INPUT' && (type === 'submit' || type === 'image'));
|
|
403
395
|
if (isSubmitButton && this.form) {
|
|
404
|
-
|
|
396
|
+
// Lightpanda raises a JsException from requestSubmit when a
|
|
397
|
+
// bubble-phase listener (e.g. Turbo's submitBubbled) calls
|
|
398
|
+
// preventDefault + stopImmediatePropagation on the SubmitEvent.
|
|
399
|
+
// Per HTML spec a cancelled submission should be a silent no-op.
|
|
400
|
+
// Log unexpected errors via console.warn so they remain
|
|
401
|
+
// diagnosable (LIGHTPANDA_DEBUG surfaces console output) instead
|
|
402
|
+
// of silently swallowing future regressions.
|
|
403
|
+
try {
|
|
404
|
+
this.form.requestSubmit(this);
|
|
405
|
+
} catch (e) {
|
|
406
|
+
try { console.warn('[capybara-lightpanda] requestSubmit threw:', e && e.message ? e.message : e); } catch (_) {}
|
|
407
|
+
}
|
|
405
408
|
} else if (tag === 'A' && this.href && this.target !== '_blank') {
|
|
406
|
-
|
|
409
|
+
// Same-document fragment-only navigation: just update hash (or do
|
|
410
|
+
// nothing if identical). Mirrors Chrome — assigning location.href
|
|
411
|
+
// to a same-document URL on Lightpanda triggers a real navigation
|
|
412
|
+
// tick that cancels pending setTimeout callbacks and clears form
|
|
413
|
+
// values, which breaks any test driving DOM updates from a click
|
|
414
|
+
// handler on `<a href="#...">`.
|
|
415
|
+
var dest = new URL(this.href, document.baseURI);
|
|
416
|
+
var here = new URL(window.location.href);
|
|
417
|
+
if (dest.origin === here.origin && dest.pathname === here.pathname &&
|
|
418
|
+
dest.search === here.search) {
|
|
419
|
+
if (dest.hash !== here.hash) {
|
|
420
|
+
window.location.hash = dest.hash;
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
window.location.href = this.href;
|
|
424
|
+
}
|
|
407
425
|
}
|
|
408
426
|
}
|
|
409
427
|
JS
|
|
@@ -8,7 +8,7 @@ module Capybara
|
|
|
8
8
|
READY_PATTERN = /server running.*address\s*=\s*(\d+\.\d+\.\d+\.\d+:\d+)/m
|
|
9
9
|
ADDRESS_IN_USE_PATTERN = /err=AddressInUse/
|
|
10
10
|
|
|
11
|
-
# Floor for the cookie/navigation/redirect/modal/keyboard/css/forms
|
|
11
|
+
# Floor for the cookie/navigation/redirect/modal/keyboard/css/forms/dispatch
|
|
12
12
|
# fixes the gem now relies on: PR #2255 (Network.clearBrowserCookies
|
|
13
13
|
# empty params + Network.getAllCookies), PR #2257
|
|
14
14
|
# (window.location.pathname/.search assignment triggers navigation),
|
|
@@ -23,10 +23,12 @@ module Capybara
|
|
|
23
23
|
# on labeled control), PR #2286 (HTML constraint validation API:
|
|
24
24
|
# el.validity, validationMessage, checkValidity, reportValidity),
|
|
25
25
|
# PR #2342 (<summary> click toggles parent <details>.open),
|
|
26
|
-
# PR #2352 (HTMLInputElement.pattern + patternMismatch via V8 RegExp)
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
26
|
+
# PR #2352 (HTMLInputElement.pattern + patternMismatch via V8 RegExp),
|
|
27
|
+
# PR #2368 (events: report listener exceptions instead of halting
|
|
28
|
+
# dispatch — lets us drop the polyfills.js patchDispatch IIFE).
|
|
29
|
+
# Build 6065 = main HEAD 61364437 (2026-05-06, PR #2368 merge);
|
|
30
|
+
# ships in nightly published 2026-05-06 ~03:30 UTC for all four platforms.
|
|
31
|
+
MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6065")
|
|
30
32
|
|
|
31
33
|
attr_reader :pid, :ws_url, :version, :nightly_build
|
|
32
34
|
|