capybara-lightpanda 0.3.0 → 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.
@@ -9,9 +9,9 @@ module Capybara
9
9
  ADDRESS_IN_USE_PATTERN = /err=AddressInUse/
10
10
 
11
11
  # Floor for the cookie/navigation/redirect/modal/keyboard/css/forms/dispatch/
12
- # xpath/history fixes the gem now relies on: PR #2255 (Network.clearBrowserCookies
13
- # empty params + Network.getAllCookies), PR #2257
14
- # (window.location.pathname/.search assignment triggers navigation),
12
+ # xpath/history/iframe-context/dialog fixes the gem now relies on:
13
+ # PR #2255 (Network.clearBrowserCookies empty params + Network.getAllCookies),
14
+ # PR #2257 (window.location.pathname/.search assignment triggers navigation),
15
15
  # PR #2265 (URL fragment inherited across fragment-less redirect),
16
16
  # PR #2261 (LP.handleJavaScriptDialog pre-arm), PR #2283 (Referer on
17
17
  # cross-page nav), PR #2292 (KeyboardEvent.keyCode/charCode), PR #2294
@@ -25,15 +25,37 @@ module Capybara
25
25
  # PR #2342 (<summary> click toggles parent <details>.open),
26
26
  # PR #2352 (HTMLInputElement.pattern + patternMismatch via V8 RegExp),
27
27
  # PR #2368 (events: report listener exceptions instead of halting
28
- # dispatch — lets us drop the polyfills.js patchDispatch IIFE),
28
+ # dispatch — load-bearing for the gem's JS bundle dispatch assumptions),
29
29
  # PR #2289 (Page.getNavigationHistory + Page.navigateToHistoryEntry —
30
30
  # lets us drop the history.back()/history.forward() JS workaround in
31
31
  # Browser#back / #forward), PR #2305 (XPath 1.0: Document.evaluate,
32
32
  # XPathResult, XPathEvaluator, XPathExpression — lets us drop the
33
- # ~700 LOC XPath polyfill in javascripts/index.js).
34
- # Build 6109 = main HEAD cfcfe4ee (2026-05-11, after PR #2289 and
35
- # PR #2305 merges); will ship in nightly published 2026-05-12 ~03:30 UTC.
36
- MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6109")
33
+ # ~700 LOC XPath polyfill in javascripts/index.js),
34
+ # PR #2431 (cdp: remove duplicate Page.frameNavigated emission + reuse
35
+ # child frame's V8 context fixes issue #2400 iframe contextId churn,
36
+ # lets us drop Browser#find_in_frame's refresh_frame_stack! rescue),
37
+ # PR #2445 (cdp: reset browser context arena on Target.disposeBrowserContext
38
+ # — restores per-spec state hygiene during Driver#reset!, cures the
39
+ # batch-mode pollution that PR #2431 alone exposed),
40
+ # PR #2435 (dom: implement HTMLDialogElement.{show, showModal, close}
41
+ # natively — load-bearing for the gem's HTMLDialogElement assumptions
42
+ # after polyfills.js was deleted),
43
+ # PR #2450 (forms: add enctype + 5 submitter form-* IDL accessors +
44
+ # text/plain submission — lets us delete polyfills.js entirely; reads
45
+ # of form.enctype / submitter.form{Action,Enctype,Method,NoValidate,
46
+ # Target} now return spec-typed values natively),
47
+ # PR #2478 (css: evaluate @media and matchMedia against viewport —
48
+ # inline <style> @media blocks now apply declarations against the
49
+ # hardcoded 1920×1080 viewport, and window.matchMedia(q).matches
50
+ # returns spec-correct booleans. Lets _lightpanda.isVisible detect
51
+ # inline-@media-gated hides via el.checkVisibility() without any
52
+ # gem-side workaround. External <link rel="stylesheet"> fetch stays
53
+ # out of scope by design — see .claude/rules/lightpanda-io.md
54
+ # limitation #6).
55
+ # Build 6269 = first nightly carrying PR #2478 (merge commit
56
+ # ab63cfbf, 2026-05-16); the 2026-05-16 nightly cut at 03:36 UTC
57
+ # was hours before the merge at 13:42 UTC.
58
+ MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6269")
37
59
 
38
60
  attr_reader :pid, :ws_url, :version, :nightly_build
39
61
 
@@ -47,10 +69,11 @@ module Capybara
47
69
  @stdout_w = nil
48
70
  @stderr_r = nil
49
71
  @stderr_w = nil
72
+ @finalizer_registered = false
50
73
  end
51
74
 
52
75
  def start
53
- binary_path = @options.browser_path || Binary.find_or_download
76
+ binary_path = @options.browser_path || Binary.update
54
77
 
55
78
  raise BinaryNotFoundError, "Lightpanda binary not found" unless binary_path
56
79
 
@@ -170,7 +193,7 @@ module Capybara
170
193
  end
171
194
 
172
195
  def build_args
173
- [
196
+ base = [
174
197
  "serve",
175
198
  "--host",
176
199
  @options.host.to_s,
@@ -179,6 +202,8 @@ module Capybara
179
202
  "--log_level",
180
203
  "info",
181
204
  ]
205
+ extra = ENV.fetch("LIGHTPANDA_EXTRA_ARGS", "").split
206
+ base + extra
182
207
  end
183
208
 
184
209
  def wait_for_ready
@@ -257,17 +282,29 @@ module Capybara
257
282
  end
258
283
 
259
284
  # Returns an array of PIDs holding the TCP port, [] if none, or nil if
260
- # `lsof` itself isn't available on this system.
285
+ # `lsof` itself isn't available / usable on this system.
286
+ #
287
+ # `lsof -ti` exits 1 with empty stdout/stderr when nothing matches the
288
+ # filter — that's the common "port not held" case, so we treat
289
+ # (exit != 0, empty stdout, empty stderr) as []. A non-zero exit with
290
+ # stderr content is a real lsof failure (broken install, permission
291
+ # error, etc.); surface that as `nil` so the caller raises a clear
292
+ # BinaryError instead of silently retrying the start.
261
293
  def pids_listening_on(port)
262
- stdout, _, status = Open3.capture3("lsof", "-ti", "tcp:#{port}")
263
- return [] unless status.success?
294
+ stdout, stderr, status = Open3.capture3("lsof", "-ti", "tcp:#{port}")
295
+ return parse_lsof_pids(stdout) if status.success?
296
+ return [] if stdout.strip.empty? && stderr.strip.empty?
264
297
 
298
+ nil
299
+ rescue Errno::ENOENT
300
+ nil
301
+ end
302
+
303
+ def parse_lsof_pids(stdout)
265
304
  stdout.split("\n").filter_map do |line|
266
305
  pid = line.strip.to_i
267
306
  pid.positive? ? pid : nil
268
307
  end
269
- rescue Errno::ENOENT
270
- nil
271
308
  end
272
309
 
273
310
  # Class method so the finalizer proc doesn't capture `self` (which
@@ -285,8 +322,19 @@ module Capybara
285
322
  end
286
323
  end
287
324
 
325
+ # `start` may be called more than once on the same Process instance
326
+ # (Browser#restart_process_if_dead runs `stop` then `start` after a
327
+ # crash). Each `attempt_start` calls `register_finalizer`, and
328
+ # ObjectSpace allows multiple finalizers per object — so without
329
+ # this guard the second start would queue a redundant TERM-on-GC
330
+ # whose first invocation no-ops on ESRCH but is still pure noise.
331
+ # We register exactly once; the captured `pid` is overwritten by
332
+ # `undefine_finalizer + define_finalizer` so the finalizer always
333
+ # references the most recently started process.
288
334
  def register_finalizer(pid)
335
+ ObjectSpace.undefine_finalizer(self) if @finalizer_registered
289
336
  ObjectSpace.define_finalizer(self, self.class.send(:weak_kill, pid))
337
+ @finalizer_registered = true
290
338
  end
291
339
 
292
340
  def cleanup_pipes
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara-lightpanda"
4
+
5
+ namespace :lightpanda do
6
+ namespace :binary do
7
+ desc "Print the version of the cached Lightpanda binary"
8
+ task :version do
9
+ version = Capybara::Lightpanda::Binary.current_version
10
+ if version
11
+ puts version
12
+ else
13
+ warn "No cached Lightpanda binary at #{Capybara::Lightpanda::Binary.install_path}"
14
+ exit 1
15
+ end
16
+ end
17
+
18
+ desc "Download the Lightpanda binary (optionally pinned: rake lightpanda:binary:update[0.3.0])"
19
+ task :update, [:version] do |_, args|
20
+ Capybara::Lightpanda::Binary.required_version = args[:version] if args[:version]
21
+ path = Capybara::Lightpanda::Binary.update
22
+ puts "Lightpanda binary ready at #{path}"
23
+ end
24
+
25
+ desc "Remove the cached Lightpanda binary"
26
+ task :remove do
27
+ removed = Capybara::Lightpanda::Binary.remove
28
+ if removed
29
+ puts "Removed #{removed}"
30
+ else
31
+ puts "Nothing to remove at #{Capybara::Lightpanda::Binary.install_path}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Lightpanda
5
+ module Utils
6
+ # Block-based polling helper. Borrowed from selenium-webdriver's
7
+ # Wait class (rb/lib/selenium/webdriver/common/wait.rb). Sibling
8
+ # of Utils::Attempt — Attempt retries on a specific error class,
9
+ # Wait loops until the block returns truthy.
10
+ module Wait
11
+ DEFAULT_INTERVAL = 0.1
12
+
13
+ # Polls the block until it returns a truthy value or `timeout`
14
+ # seconds elapse. Exceptions whose class is listed in `ignore`
15
+ # are swallowed between polls; the most recent one is appended
16
+ # to the timeout message so the failure stays diagnosable.
17
+ #
18
+ # @raise [Capybara::Lightpanda::TimeoutError] if the block never
19
+ # returns truthy within `timeout` seconds.
20
+ # @return the truthy value returned by the block.
21
+ def self.until(timeout:, interval: DEFAULT_INTERVAL, ignore: [], message: nil)
22
+ deadline = monotonic_time + timeout
23
+ last_error = nil
24
+ loop do
25
+ begin
26
+ result = yield
27
+ return result if result
28
+ rescue *Array(ignore) => e
29
+ last_error = e
30
+ end
31
+
32
+ break if monotonic_time > deadline
33
+
34
+ sleep interval
35
+ end
36
+
37
+ msg = message || "timed out after #{timeout}s"
38
+ msg = "#{msg} (#{last_error.message})" if last_error
39
+ raise Capybara::Lightpanda::TimeoutError, msg
40
+ end
41
+
42
+ def self.monotonic_time
43
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Lightpanda
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -10,14 +10,16 @@ require_relative "capybara/lightpanda/binary"
10
10
  require_relative "capybara/lightpanda/process"
11
11
  require_relative "capybara/lightpanda/utils/event"
12
12
  require_relative "capybara/lightpanda/utils/attempt"
13
+ require_relative "capybara/lightpanda/utils/wait"
13
14
  require_relative "capybara/lightpanda/client"
15
+ require_relative "capybara/lightpanda/headers"
14
16
  require_relative "capybara/lightpanda/network"
15
17
  require_relative "capybara/lightpanda/cookies"
16
18
  require_relative "capybara/lightpanda/keyboard"
17
- require_relative "capybara/lightpanda/frame"
18
19
  require_relative "capybara/lightpanda/browser"
19
- require_relative "capybara/lightpanda/xpath_polyfill"
20
+ require_relative "capybara/lightpanda/auto_scripts"
20
21
  require_relative "capybara/lightpanda/node"
22
+ require_relative "capybara/lightpanda/element_extension"
21
23
  require_relative "capybara/lightpanda/driver"
22
24
 
23
25
  module Capybara
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.3.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/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
@@ -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