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.
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.2.2
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/frame.rb
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.6
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