capybara-lightpanda 0.2.2 → 0.4.0
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 +54 -0
- data/README.md +3 -0
- data/lib/capybara/lightpanda/auto_scripts.rb +10 -0
- data/lib/capybara/lightpanda/binary.rb +111 -49
- data/lib/capybara/lightpanda/browser.rb +268 -177
- data/lib/capybara/lightpanda/client/web_socket.rb +24 -4
- data/lib/capybara/lightpanda/client.rb +13 -0
- data/lib/capybara/lightpanda/cookies.rb +8 -2
- data/lib/capybara/lightpanda/driver.rb +26 -4
- data/lib/capybara/lightpanda/element_extension.rb +21 -0
- data/lib/capybara/lightpanda/headers.rb +15 -0
- data/lib/capybara/lightpanda/javascripts/index.js +30 -802
- data/lib/capybara/lightpanda/keyboard.rb +18 -1
- data/lib/capybara/lightpanda/network.rb +50 -21
- data/lib/capybara/lightpanda/node.rb +72 -51
- data/lib/capybara/lightpanda/process.rb +68 -15
- data/lib/capybara/lightpanda/tasks/binary.rake +35 -0
- data/lib/capybara/lightpanda/utils/wait.rb +48 -0
- data/lib/capybara/lightpanda/version.rb +1 -1
- data/lib/capybara-lightpanda.rb +4 -2
- metadata +7 -5
- data/lib/capybara/lightpanda/frame.rb +0 -33
- data/lib/capybara/lightpanda/javascripts/polyfills.js +0 -212
- data/lib/capybara/lightpanda/xpath_polyfill.rb +0 -15
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: capybara-lightpanda
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Navid Emad
|
|
@@ -71,6 +71,7 @@ files:
|
|
|
71
71
|
- NOTICE.md
|
|
72
72
|
- README.md
|
|
73
73
|
- lib/capybara-lightpanda.rb
|
|
74
|
+
- lib/capybara/lightpanda/auto_scripts.rb
|
|
74
75
|
- lib/capybara/lightpanda/binary.rb
|
|
75
76
|
- lib/capybara/lightpanda/browser.rb
|
|
76
77
|
- lib/capybara/lightpanda/client.rb
|
|
@@ -78,20 +79,21 @@ files:
|
|
|
78
79
|
- lib/capybara/lightpanda/client/web_socket.rb
|
|
79
80
|
- lib/capybara/lightpanda/cookies.rb
|
|
80
81
|
- lib/capybara/lightpanda/driver.rb
|
|
82
|
+
- lib/capybara/lightpanda/element_extension.rb
|
|
81
83
|
- lib/capybara/lightpanda/errors.rb
|
|
82
|
-
- lib/capybara/lightpanda/
|
|
84
|
+
- lib/capybara/lightpanda/headers.rb
|
|
83
85
|
- lib/capybara/lightpanda/javascripts/index.js
|
|
84
|
-
- lib/capybara/lightpanda/javascripts/polyfills.js
|
|
85
86
|
- lib/capybara/lightpanda/keyboard.rb
|
|
86
87
|
- lib/capybara/lightpanda/logger.rb
|
|
87
88
|
- lib/capybara/lightpanda/network.rb
|
|
88
89
|
- lib/capybara/lightpanda/node.rb
|
|
89
90
|
- lib/capybara/lightpanda/options.rb
|
|
90
91
|
- lib/capybara/lightpanda/process.rb
|
|
92
|
+
- lib/capybara/lightpanda/tasks/binary.rake
|
|
91
93
|
- lib/capybara/lightpanda/utils/attempt.rb
|
|
92
94
|
- lib/capybara/lightpanda/utils/event.rb
|
|
95
|
+
- lib/capybara/lightpanda/utils/wait.rb
|
|
93
96
|
- lib/capybara/lightpanda/version.rb
|
|
94
|
-
- lib/capybara/lightpanda/xpath_polyfill.rb
|
|
95
97
|
homepage: https://navidemad.github.io/capybara-lightpanda
|
|
96
98
|
licenses:
|
|
97
99
|
- MIT
|
|
@@ -115,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
117
|
- !ruby/object:Gem::Version
|
|
116
118
|
version: '0'
|
|
117
119
|
requirements: []
|
|
118
|
-
rubygems_version: 4.0.
|
|
120
|
+
rubygems_version: 4.0.10
|
|
119
121
|
specification_version: 4
|
|
120
122
|
summary: Capybara driver for the Lightpanda headless browser
|
|
121
123
|
test_files: []
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Capybara
|
|
4
|
-
module Lightpanda
|
|
5
|
-
# Lightweight metadata view of a CDP frame, populated from
|
|
6
|
-
# Page.frameAttached / Page.frameNavigated / Page.frame{Started,Stopped}Loading
|
|
7
|
-
# events. Mirrors a subset of ferrum's Frame.
|
|
8
|
-
#
|
|
9
|
-
# NOTE: this is purely introspection — Lightpanda's frame loading events
|
|
10
|
-
# are not reliable enough to drive `wait_for_navigation` (#1801, #1832),
|
|
11
|
-
# so the gem still drives navigation waits via Page.loadEventFired with
|
|
12
|
-
# readyState polling. The frame map is useful for diagnostics, listing
|
|
13
|
-
# iframes, and resolving frame metadata (name/URL) without callFunctionOn.
|
|
14
|
-
class Frame
|
|
15
|
-
STATES = %i[started_loading navigated stopped_loading detached].freeze
|
|
16
|
-
|
|
17
|
-
attr_reader :id, :parent_id
|
|
18
|
-
attr_accessor :name, :url, :state
|
|
19
|
-
|
|
20
|
-
def initialize(id, parent_id = nil, name: nil, url: nil)
|
|
21
|
-
@id = id
|
|
22
|
-
@parent_id = parent_id
|
|
23
|
-
@name = name
|
|
24
|
-
@url = url
|
|
25
|
-
@state = nil
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def main?
|
|
29
|
-
@parent_id.nil?
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// Polyfills compensant des limitations du binaire Lightpanda.
|
|
2
|
-
// Chaque section est gardée par un test de feature : dès qu'upstream implémente
|
|
3
|
-
// l'API native, le polyfill devient un no-op et peut être retiré.
|
|
4
|
-
// Voir UPSTREAM_BUGS.md à la racine du gem pour les repros et liens d'issues.
|
|
5
|
-
(function () {
|
|
6
|
-
"use strict";
|
|
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
|
-
|
|
80
|
-
// ── Bug #4 — HTMLDialogElement.{showModal, show, close} non implémentés ──
|
|
81
|
-
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element
|
|
82
|
-
if (typeof HTMLDialogElement !== "undefined") {
|
|
83
|
-
var dproto = HTMLDialogElement.prototype;
|
|
84
|
-
if (typeof dproto.showModal !== "function") {
|
|
85
|
-
dproto.showModal = function () {
|
|
86
|
-
if (this.hasAttribute("open")) {
|
|
87
|
-
throw new (window.DOMException || Error)(
|
|
88
|
-
"The element already has an 'open' attribute, and therefore cannot be opened modally.",
|
|
89
|
-
"InvalidStateError"
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
this.setAttribute("open", "");
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
if (typeof dproto.show !== "function") {
|
|
96
|
-
dproto.show = function () {
|
|
97
|
-
if (!this.hasAttribute("open")) this.setAttribute("open", "");
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (typeof dproto.close !== "function") {
|
|
101
|
-
dproto.close = function (returnValue) {
|
|
102
|
-
if (!this.hasAttribute("open")) return;
|
|
103
|
-
this.removeAttribute("open");
|
|
104
|
-
if (returnValue !== undefined) this.returnValue = String(returnValue);
|
|
105
|
-
this.dispatchEvent(new Event("close"));
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
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
|
-
}
|
|
154
|
-
|
|
155
|
-
function inScope(type, capture) {
|
|
156
|
-
return type === "submit" && capture === true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
var pending = []; // [{ target, type, fn, capture, cancelled }]
|
|
160
|
-
var flushScheduled = false;
|
|
161
|
-
|
|
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;
|
|
172
|
-
try {
|
|
173
|
-
origRemove.call(r.target, r.type, r.fn, r.capture);
|
|
174
|
-
} catch (_) {}
|
|
175
|
-
}
|
|
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
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return origAdd.call(this, type, fn, opts);
|
|
210
|
-
};
|
|
211
|
-
})();
|
|
212
|
-
})();
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Capybara
|
|
4
|
-
module Lightpanda
|
|
5
|
-
module XPathPolyfill
|
|
6
|
-
JS_PATH = File.expand_path("javascripts/index.js", __dir__).freeze
|
|
7
|
-
JS = File.read(JS_PATH).freeze
|
|
8
|
-
|
|
9
|
-
# Polyfills pour les APIs DOM manquantes du binaire Lightpanda.
|
|
10
|
-
# Voir UPSTREAM_BUGS.md à la racine du gem.
|
|
11
|
-
POLYFILLS_PATH = File.expand_path("javascripts/polyfills.js", __dir__).freeze
|
|
12
|
-
POLYFILLS_JS = File.read(POLYFILLS_PATH).freeze
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|