capybara-lightpanda 0.7.0 → 0.8.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 +23 -0
- data/lib/capybara/lightpanda/binary.rb +47 -5
- data/lib/capybara/lightpanda/browser.rb +69 -29
- data/lib/capybara/lightpanda/cookies.rb +2 -0
- data/lib/capybara/lightpanda/driver.rb +23 -0
- data/lib/capybara/lightpanda/javascripts/index.js +0 -13
- data/lib/capybara/lightpanda/network.rb +3 -0
- data/lib/capybara/lightpanda/node.rb +7 -0
- data/lib/capybara/lightpanda/process.rb +10 -4
- data/lib/capybara/lightpanda/railtie.rb +15 -0
- data/lib/capybara/lightpanda/version.rb +1 -1
- data/lib/capybara-lightpanda.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f039526ec34b172b3824416cd6b7679720cabcb75455a1c3c46bbf0cc11b5d7b
|
|
4
|
+
data.tar.gz: '087e752b5baa7d09d43541e90654732e042f28535d53248fab9ab2769f656c01'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2430399381dbcb55f54eafbbb9fb81cc3c4ac5956e4bbc16d5dfe70fb1f347f7f8dfdf60044134c87ae739200911d22e986ce9b4d0a37a2f21ebf74887e50f1
|
|
7
|
+
data.tar.gz: 500584b0da1e6bd82737e213fdd13e10395334e0c971923e2f7fdcb78758251f36f50361272b66c1838a781bf134b724c7b1cc77761dfb28a9b6cfbd8de65d31
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-06-12
|
|
4
|
+
|
|
5
|
+
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6736 (published 2026-06-12). The driver refuses to start against older binaries.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `lightpanda:binary:*` rake tasks now load automatically inside Rails apps (via a Railtie), and the "Lightpanda is too old" error suggests a require-the-gem one-liner instead of those rake tasks. The old hint was a dead end: the tasks weren't loaded in Rails apps at all, and even with the Railtie they only exist under `RAILS_ENV=test` when the gem sits in the `:test` Gemfile group. (Found beta-testing a private Rails suite.)
|
|
10
|
+
- `Driver#render` as an alias of `save_screenshot`. capybara-screenshot calls `driver.render(path)` for drivers it doesn't know, so every failing test in a capybara-screenshot suite logged "Screenshot could not be saved: undefined method 'render'" — once per failure.
|
|
11
|
+
- `Driver#headers=` / `#add_headers` / `#headers`, delegating to the existing `Network` support. Cuprite exposes header writers on the driver and real suites call them there (`page.driver.headers = …`).
|
|
12
|
+
- `Cookies#[]` as an alias of `#get` — the Ferrum/Cuprite spelling (`browser.cookies["session_id"]`).
|
|
13
|
+
- `Browser#console_logs` / `#clear_console_logs` — console messages captured since the last session reset, as `{type:, text:, timestamp:, args:}` hashes (driver-internal Turbo sentinels excluded, buffer capped at 1,000 entries). Suites that assert "no JS errors leaked" no longer need to wire a custom Ferrum-style logger: `page.driver.browser.console_logs.select { |m| m[:type] == "error" }`. Note that Lightpanda currently reports both `console.log` and `console.warn` as type `"info"` — filter on `text` when you need to distinguish them.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Modal assertions match dialogs by message text regardless of the alert/confirm/prompt type, like Selenium and Cuprite — `accept_alert` around a `data-confirm` delete button now works.
|
|
18
|
+
- Clicks dispatch the full pointer sequence (mousedown → mouseup → click). Widgets that open on mousedown — select2 v3 dropdowns, for example — now react to `click` / `select_from`.
|
|
19
|
+
- The driver no longer re-dispatches `readystatechange` itself. Lightpanda fires the event natively as of nightly 6736 (lightpanda-io/browser#2708), so the shim added in 0.7.0 became redundant and was removed. Behavior is unchanged for Turbo/Hotwire apps — `turbo:load` still fires on every visit, now from the browser's own event.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- A failed binary download that falls back to a stale cached binary now warns loudly on stderr (with the original error and a re-provision one-liner) instead of logging only under `LIGHTPANDA_DEBUG`. VCR-guarded suites hit this path silently — VCR's blocking error is a `StandardError`, unlike raw WebMock's — and the only visible symptom was a confusing "Lightpanda is too old" error much later.
|
|
24
|
+
- Turbo Streams over ActionCable now connect. Page-initiated WebSocket upgrades carry the document's `Origin` header as of nightly 6736 (lightpanda-io/browser#2710, guaranteed by the new minimum build), so ActionCable's request-forgery protection accepts the connection instead of rejecting it with `Request origin not allowed: nil`. `turbo-cable-stream-source` elements reach `[connected]` and `turbo_stream_for` broadcasts (solid_cable or any adapter) arrive in specs. If you had added `config.action_cable.disable_request_forgery_protection = true` to your test environment to work around this, you can remove it.
|
|
25
|
+
|
|
3
26
|
## [0.7.0] - 2026-06-12
|
|
4
27
|
|
|
5
28
|
### Changed
|
|
@@ -33,6 +33,13 @@ module Capybara
|
|
|
33
33
|
|
|
34
34
|
DEFAULT_CACHE_TIME = 86_400
|
|
35
35
|
|
|
36
|
+
# One-liner that re-provisions the binary from a process with no
|
|
37
|
+
# HTTP-stubbing loaded (VCR/WebMock guard the test process itself).
|
|
38
|
+
# Referenced from the stale-fallback warning and BETA_TESTING.md.
|
|
39
|
+
PROVISION_HINT =
|
|
40
|
+
"bundle exec ruby -r capybara-lightpanda " \
|
|
41
|
+
"-e 'Capybara::Lightpanda::Binary.remove; puts Capybara::Lightpanda::Binary.update'"
|
|
42
|
+
|
|
36
43
|
class << self
|
|
37
44
|
# Set a specific release tag (e.g. "0.3.0") to pin downloads to that
|
|
38
45
|
# release. When nil, the rolling "nightly" tag is used. The pin only
|
|
@@ -96,7 +103,36 @@ module Capybara
|
|
|
96
103
|
return system_path
|
|
97
104
|
end
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
# Stale-or-absent cache, nothing on PATH: refresh from the network.
|
|
107
|
+
# If that fails (GitHub 5xx, DNS/connect timeouts, SocketError) but a
|
|
108
|
+
# usable — if stale — binary is already cached, keep using it rather
|
|
109
|
+
# than hard-failing. A cold cache (nothing on disk) still surfaces the
|
|
110
|
+
# error. The MINIMUM_NIGHTLY_BUILD floor is enforced downstream in
|
|
111
|
+
# Process#start, so a sub-floor binary can't slip in.
|
|
112
|
+
#
|
|
113
|
+
# Deliberately StandardError, not Exception: WebMock's
|
|
114
|
+
# NetConnectNotAllowedError descends from Exception so it propagates
|
|
115
|
+
# through app rescue blocks by design — a test suite that blocks net
|
|
116
|
+
# connections SHOULD fail loudly here, not silently fall back. CI
|
|
117
|
+
# pre-provisions the binary outside that guard instead (real-apps.yml).
|
|
118
|
+
begin
|
|
119
|
+
download
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
raise unless File.executable?(destination)
|
|
122
|
+
|
|
123
|
+
# Kernel.warn, not log: log() is silent unless LIGHTPANDA_DEBUG or
|
|
124
|
+
# an explicit logger is set, and this fallback is exactly the
|
|
125
|
+
# moment the user needs to hear about — a VCR-guarded suite (whose
|
|
126
|
+
# UnhandledHTTPRequestError is a StandardError, unlike raw
|
|
127
|
+
# WebMock's Exception) lands here silently, keeps a stale binary,
|
|
128
|
+
# and later hits a confusing MINIMUM_NIGHTLY_BUILD floor error
|
|
129
|
+
# with no trace of the blocked download.
|
|
130
|
+
warn("[capybara-lightpanda] Binary download failed (#{e.class}: #{e.message}); " \
|
|
131
|
+
"falling back to the cached binary at #{destination}. " \
|
|
132
|
+
"If your suite stubs HTTP (VCR/WebMock), pre-provision from an " \
|
|
133
|
+
"unstubbed process: #{PROVISION_HINT}")
|
|
134
|
+
destination
|
|
135
|
+
end
|
|
100
136
|
end
|
|
101
137
|
|
|
102
138
|
# Delete the cached binary. Returns the path that was deleted, or nil
|
|
@@ -173,9 +209,15 @@ module Capybara
|
|
|
173
209
|
# suggest `brew update && brew upgrade lightpanda` (brew pins
|
|
174
210
|
# each user's binary at install time and doesn't refresh on its
|
|
175
211
|
# own when the tap publishes a newer nightly).
|
|
176
|
-
# - Path equals our own cache → suggest the gem
|
|
177
|
-
#
|
|
178
|
-
#
|
|
212
|
+
# - Path equals our own cache → suggest the require-the-gem one-liner.
|
|
213
|
+
# NOT the lightpanda:binary:* rake tasks: in a Rails app the gem
|
|
214
|
+
# usually sits in the :test Gemfile group, so the tasks only exist
|
|
215
|
+
# under RAILS_ENV=test (the Railtie can't help a plain `bundle exec
|
|
216
|
+
# rake` in development), and outside Rails they're never loaded at
|
|
217
|
+
# all. The one-liner requires the gem explicitly, so it works from
|
|
218
|
+
# any environment. The `remove` step is required because `update`
|
|
219
|
+
# honors `cache_time` and would otherwise no-op on a
|
|
220
|
+
# too-old-but-not-yet-expired file.
|
|
179
221
|
# - Anything else (user-managed install at a custom path) → keep
|
|
180
222
|
# the curl-overwrite suggestion, since we don't know how the file
|
|
181
223
|
# got there.
|
|
@@ -183,7 +225,7 @@ module Capybara
|
|
|
183
225
|
if brew_managed?(binary_path)
|
|
184
226
|
"brew update && brew upgrade lightpanda"
|
|
185
227
|
elsif binary_path == install_path
|
|
186
|
-
|
|
228
|
+
PROVISION_HINT
|
|
187
229
|
else
|
|
188
230
|
"curl -sL #{GITHUB_RELEASE_URL}/nightly/#{platform_binary} " \
|
|
189
231
|
"-o #{binary_path} && chmod +x #{binary_path}"
|
|
@@ -77,6 +77,8 @@ module Capybara
|
|
|
77
77
|
@modal_messages = []
|
|
78
78
|
@modal_messages_mutex = Mutex.new
|
|
79
79
|
@modal_handler_installed = false
|
|
80
|
+
@console_logs = []
|
|
81
|
+
@console_logs_mutex = Mutex.new
|
|
80
82
|
@frame_stack = []
|
|
81
83
|
@turbo_event = Utils::Event.new
|
|
82
84
|
@turbo_event.set
|
|
@@ -124,6 +126,7 @@ module Capybara
|
|
|
124
126
|
|
|
125
127
|
@turbo_event.set
|
|
126
128
|
subscribe_to_console_logs
|
|
129
|
+
subscribe_to_console_capture
|
|
127
130
|
subscribe_to_execution_context
|
|
128
131
|
subscribe_to_turbo_signals
|
|
129
132
|
subscribe_to_navigation_response
|
|
@@ -172,6 +175,7 @@ module Capybara
|
|
|
172
175
|
@page_events_enabled = false
|
|
173
176
|
@modal_handler_installed = false
|
|
174
177
|
@modal_messages_mutex.synchronize { @modal_messages.clear }
|
|
178
|
+
@console_logs_mutex.synchronize { @console_logs.clear }
|
|
175
179
|
@last_navigation_response = nil
|
|
176
180
|
@document_request_id = nil
|
|
177
181
|
clear_frames
|
|
@@ -588,6 +592,22 @@ module Capybara
|
|
|
588
592
|
@cookies ||= Cookies.new(self)
|
|
589
593
|
end
|
|
590
594
|
|
|
595
|
+
# Console messages captured from `Runtime.consoleAPICalled` since the
|
|
596
|
+
# last `reset` (Turbo-tracker sentinels excluded). Loose hashes, like
|
|
597
|
+
# Network#traffic: `{type:, text:, timestamp:, args:}` where `type` is
|
|
598
|
+
# the console method name ("log", "error", "warning", ...), `text` joins
|
|
599
|
+
# the arguments' primitive values/descriptions, and `args` keeps the raw
|
|
600
|
+
# CDP RemoteObjects. Lets suites assert on JS console errors
|
|
601
|
+
# (`browser.console_logs.select { |m| m[:type] == "error" }`) the way
|
|
602
|
+
# peer drivers do via custom Ferrum loggers.
|
|
603
|
+
def console_logs
|
|
604
|
+
@console_logs_mutex.synchronize { @console_logs.dup }
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def clear_console_logs
|
|
608
|
+
@console_logs_mutex.synchronize { @console_logs.clear }
|
|
609
|
+
end
|
|
610
|
+
|
|
591
611
|
# -- Frame Support --
|
|
592
612
|
# `frame_stack` (Array<Node>) is the Capybara `switch_to_frame` stack;
|
|
593
613
|
# it drives where `find` resolves selectors. Stored as Nodes so
|
|
@@ -640,61 +660,52 @@ module Capybara
|
|
|
640
660
|
page_command("LP.handleJavaScriptDialog", accept: false)
|
|
641
661
|
end
|
|
642
662
|
|
|
663
|
+
# `type` is accepted for the error message only: like Selenium (where
|
|
664
|
+
# alert/confirm are indistinguishable) and Cuprite (whose dialog handler
|
|
665
|
+
# accepts whatever fires), we deliberately do NOT reject a dialog whose
|
|
666
|
+
# reported type differs from the one Capybara asked for. Real suites
|
|
667
|
+
# wrap `data-confirm` deletes in `accept_alert` (e.g. solidus admin) and
|
|
668
|
+
# expect it to work; only the message text is matched.
|
|
643
669
|
def find_modal(type, text: nil, wait: options.timeout)
|
|
644
670
|
regexp = text.is_a?(Regexp) ? text : (text && Regexp.new(Regexp.escape(text.to_s)))
|
|
645
|
-
last_matching_type_message = nil
|
|
646
671
|
last_seen_message = nil
|
|
647
672
|
claimed = nil
|
|
648
673
|
Utils::Wait.until(timeout: wait, interval: 0.05) do
|
|
649
|
-
claimed = pop_modal_message(
|
|
674
|
+
claimed = pop_modal_message(regexp)
|
|
650
675
|
next true if claimed
|
|
651
676
|
|
|
652
|
-
|
|
653
|
-
last_matching_type_message = last[:matching_type] || last_matching_type_message
|
|
654
|
-
last_seen_message = last[:any] || last_seen_message
|
|
677
|
+
last_seen_message = peek_last_modal_message || last_seen_message
|
|
655
678
|
false
|
|
656
679
|
end
|
|
657
680
|
claimed[:message]
|
|
658
681
|
rescue TimeoutError
|
|
659
|
-
raise_modal_not_found(type, text,
|
|
682
|
+
raise_modal_not_found(type, text, last_seen_message)
|
|
660
683
|
end
|
|
661
684
|
|
|
662
685
|
private
|
|
663
686
|
|
|
664
|
-
# Pop the first queued dialog whose
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
def pop_modal_message(
|
|
687
|
+
# Pop the first queued dialog whose message matches the requested
|
|
688
|
+
# pattern (any dialog when `regexp` is nil). Returns the entry or nil.
|
|
689
|
+
# Serialized with the message-thread writer.
|
|
690
|
+
def pop_modal_message(regexp)
|
|
668
691
|
@modal_messages_mutex.synchronize do
|
|
669
692
|
match = @modal_messages.find do |m|
|
|
670
|
-
|
|
693
|
+
regexp.nil? || m[:message].to_s.match?(regexp)
|
|
671
694
|
end
|
|
672
695
|
@modal_messages.delete(match) if match
|
|
673
696
|
match
|
|
674
697
|
end
|
|
675
698
|
end
|
|
676
699
|
|
|
677
|
-
#
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
def peek_last_modal_message(type)
|
|
681
|
-
@modal_messages_mutex.synchronize do
|
|
682
|
-
{
|
|
683
|
-
matching_type: @modal_messages.reverse.find { |m| m[:type] == type }&.dig(:message),
|
|
684
|
-
any: @modal_messages.last&.dig(:message),
|
|
685
|
-
}
|
|
686
|
-
end
|
|
700
|
+
# Most recent dialog message of any type, for diagnostics.
|
|
701
|
+
def peek_last_modal_message
|
|
702
|
+
@modal_messages_mutex.synchronize { @modal_messages.last&.dig(:message) }
|
|
687
703
|
end
|
|
688
704
|
|
|
689
|
-
def raise_modal_not_found(type, text,
|
|
690
|
-
if
|
|
691
|
-
raise Capybara::ModalNotFound,
|
|
692
|
-
"Unable to find modal dialog with #{text} - found '#{matching_type_message}' instead."
|
|
693
|
-
end
|
|
694
|
-
if any_message
|
|
705
|
+
def raise_modal_not_found(type, text, last_seen_message)
|
|
706
|
+
if last_seen_message
|
|
695
707
|
raise Capybara::ModalNotFound,
|
|
696
|
-
"Unable to find #{type} modal
|
|
697
|
-
"a different dialog fired with message '#{any_message}'."
|
|
708
|
+
"Unable to find #{type} modal with #{text} - found '#{last_seen_message}' instead."
|
|
698
709
|
end
|
|
699
710
|
raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
|
|
700
711
|
end
|
|
@@ -865,6 +876,35 @@ module Capybara
|
|
|
865
876
|
TURBO_SENTINEL_PREFIX = "__lightpanda_turbo_"
|
|
866
877
|
private_constant :TURBO_SENTINEL_PREFIX
|
|
867
878
|
|
|
879
|
+
# Oldest entries are dropped past this cap so a chatty page can't grow
|
|
880
|
+
# the buffer unbounded across a long session.
|
|
881
|
+
CONSOLE_LOGS_LIMIT = 1_000
|
|
882
|
+
|
|
883
|
+
# Ring-buffer every console.* call for `Browser#console_logs`. Separate
|
|
884
|
+
# from subscribe_to_console_logs (which streams to an optional IO logger)
|
|
885
|
+
# so capture works without any logger configured. Skips the Turbo
|
|
886
|
+
# activity-tracker sentinels — they're driver plumbing, not page output.
|
|
887
|
+
def subscribe_to_console_capture
|
|
888
|
+
on("Runtime.consoleAPICalled") do |params|
|
|
889
|
+
args = params["args"]
|
|
890
|
+
next unless args.is_a?(Array)
|
|
891
|
+
|
|
892
|
+
first = args.first&.dig("value")
|
|
893
|
+
next if first.is_a?(String) && first.start_with?(TURBO_SENTINEL_PREFIX)
|
|
894
|
+
|
|
895
|
+
entry = {
|
|
896
|
+
type: params["type"],
|
|
897
|
+
text: args.map { |a| a.fetch("value") { a["description"] }.to_s }.join(" "),
|
|
898
|
+
timestamp: params["timestamp"],
|
|
899
|
+
args: args,
|
|
900
|
+
}
|
|
901
|
+
@console_logs_mutex.synchronize do
|
|
902
|
+
@console_logs << entry
|
|
903
|
+
@console_logs.shift(@console_logs.size - CONSOLE_LOGS_LIMIT) if @console_logs.size > CONSOLE_LOGS_LIMIT
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
|
|
868
908
|
# Wire @turbo_event to the JS-side _signalTurbo emissions. The JS calls
|
|
869
909
|
# console.debug('__lightpanda_turbo_busy') / '_idle' on transitions across
|
|
870
910
|
# zero pending ops; Lightpanda forwards those to Runtime.consoleAPICalled.
|
|
@@ -76,6 +76,8 @@ module Capybara
|
|
|
76
76
|
def get(name)
|
|
77
77
|
find { |cookie| cookie.name == name }
|
|
78
78
|
end
|
|
79
|
+
# Ferrum/Cuprite spelling: `browser.cookies["session_id"]`.
|
|
80
|
+
alias [] get
|
|
79
81
|
|
|
80
82
|
def set(name:, value:, domain: nil, path: "/", secure: false, http_only: false, # rubocop:disable Metrics/ParameterLists
|
|
81
83
|
same_site: nil, expires: nil)
|
|
@@ -213,6 +213,29 @@ module Capybara
|
|
|
213
213
|
nil
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
+
# capybara-screenshot's fallback for unregistered drivers calls
|
|
217
|
+
# `driver.render(path)` (the Cuprite/Ferrum spelling). Without the alias
|
|
218
|
+
# every failed test in a capybara-screenshot suite logged
|
|
219
|
+
# "Screenshot could not be saved: undefined method 'render'".
|
|
220
|
+
alias render save_screenshot
|
|
221
|
+
|
|
222
|
+
# -- Headers (Cuprite-compatible driver surface) --
|
|
223
|
+
# Delegates to Network, which lazily enables the Network domain and
|
|
224
|
+
# remembers the headers across reset. Cuprite exposes these on the
|
|
225
|
+
# driver, and real suites call them there (page.driver.headers = ...).
|
|
226
|
+
|
|
227
|
+
def headers
|
|
228
|
+
browser.network.extra_headers
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def headers=(headers)
|
|
232
|
+
browser.network.headers = headers
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def add_headers(headers)
|
|
236
|
+
browser.network.add_headers(headers)
|
|
237
|
+
end
|
|
238
|
+
|
|
216
239
|
# -- Lifecycle --
|
|
217
240
|
|
|
218
241
|
# Thin Cuprite-style wrapper. The interesting work — disposing the
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
(function() {
|
|
2
2
|
if (window._lightpanda) return;
|
|
3
3
|
|
|
4
|
-
// --- readystatechange re-dispatch (upstream gap, wishlist A36) ---
|
|
5
|
-
// Lightpanda transitions document.readyState correctly but never fires the
|
|
6
|
-
// readystatechange event. Turbo's PageObserver listens ONLY for that event
|
|
7
|
-
// to reach pageLoaded() — without it, turbo:load never fires. Re-dispatch
|
|
8
|
-
// from the two lifecycle events Lightpanda does fire: DOMContentLoaded
|
|
9
|
-
// (readyState=interactive) and window load (readyState=complete).
|
|
10
|
-
// Drop this block once the upstream fix is covered by MINIMUM_NIGHTLY_BUILD.
|
|
11
|
-
function _fireReadyStateChange() {
|
|
12
|
-
document.dispatchEvent(new Event('readystatechange'));
|
|
13
|
-
}
|
|
14
|
-
document.addEventListener('DOMContentLoaded', _fireReadyStateChange);
|
|
15
|
-
window.addEventListener('load', _fireReadyStateChange);
|
|
16
|
-
|
|
17
4
|
// --- Turbo activity tracking ---
|
|
18
5
|
// Tracks pending Turbo operations so the driver can wait for Turbo to settle.
|
|
19
6
|
// Inspired by the CapybaraLockstep approach for stabilizing Turbo integration tests.
|
|
@@ -55,6 +55,9 @@ module Capybara
|
|
|
55
55
|
@enabled = false
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Headers applied via headers= / add_headers. Backs Driver#headers.
|
|
59
|
+
def extra_headers = @extra_headers || {}
|
|
60
|
+
|
|
58
61
|
# Setting extra headers also lazily enables the Network domain. Without
|
|
59
62
|
# this, headers were silently ignored until the caller separately ran
|
|
60
63
|
# `network.enable` (or `wait_for_network_idle`). Cuprite/Ferrum parity.
|
|
@@ -485,6 +485,13 @@ module Capybara
|
|
|
485
485
|
CLICK_JS = <<~JS
|
|
486
486
|
function() {
|
|
487
487
|
var EventCtor = (typeof MouseEvent !== 'undefined') ? MouseEvent : Event;
|
|
488
|
+
// Real pointer clicks are a mousedown -> mouseup -> click sequence,
|
|
489
|
+
// and widgets like select2 open on `mousedown`, not `click`.
|
|
490
|
+
// Cancelling mousedown suppresses focus/text-selection in a real
|
|
491
|
+
// browser but never the click, so the click below is dispatched
|
|
492
|
+
// unconditionally.
|
|
493
|
+
this.dispatchEvent(new EventCtor('mousedown', { bubbles: true, cancelable: true }));
|
|
494
|
+
this.dispatchEvent(new EventCtor('mouseup', { bubbles: true, cancelable: true }));
|
|
488
495
|
var clickEvt = new EventCtor('click', { bubbles: true, cancelable: true });
|
|
489
496
|
var notCancelled = this.dispatchEvent(clickEvt);
|
|
490
497
|
if (!notCancelled || clickEvt.defaultPrevented) return;
|
|
@@ -90,10 +90,16 @@ module Capybara
|
|
|
90
90
|
# DragEvent, merged 2026-06-10) provides the APIs Node#drop's DROP_JS
|
|
91
91
|
# assembles its payload from; on builds without it the drop JS raises
|
|
92
92
|
# "DataTransfer is not defined".
|
|
93
|
-
# Build 6699 = the #2671 merge (d1f4c409, 2026-06-10) —
|
|
94
|
-
# floor. (The prior 6672 file-upload floor — and 6353
|
|
95
|
-
# subsumed.)
|
|
96
|
-
|
|
93
|
+
# Build 6699 = the #2671 merge (d1f4c409, 2026-06-10) — the Node#drop
|
|
94
|
+
# DataTransfer floor. (The prior 6672 file-upload floor — and 6353
|
|
95
|
+
# before it — are subsumed.)
|
|
96
|
+
# PR #2708 (document fires readystatechange on readiness changes,
|
|
97
|
+
# merged 2026-06-12) made the index.js readystatechange re-dispatch
|
|
98
|
+
# shim redundant, so it was removed — on builds without #2708 Turbo's
|
|
99
|
+
# PageObserver never reaches pageLoaded() and turbo:load never fires.
|
|
100
|
+
# Build 6736 = first published nightly carrying the #2708 merge — now
|
|
101
|
+
# the binding floor.
|
|
102
|
+
MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6736")
|
|
97
103
|
|
|
98
104
|
attr_reader :pid, :ws_url, :version, :nightly_build
|
|
99
105
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Lightpanda
|
|
5
|
+
# Exposes the gem's rake tasks (lightpanda:binary:*) inside Rails apps.
|
|
6
|
+
# Without this, the BinaryError remediation hint ("bundle exec rake
|
|
7
|
+
# lightpanda:binary:remove lightpanda:binary:update") pointed at tasks
|
|
8
|
+
# that didn't exist in the app's rake namespace.
|
|
9
|
+
class Railtie < Rails::Railtie
|
|
10
|
+
rake_tasks do
|
|
11
|
+
load File.expand_path("tasks/binary.rake", __dir__)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/capybara-lightpanda.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "capybara/lightpanda/auto_scripts"
|
|
|
21
21
|
require_relative "capybara/lightpanda/node"
|
|
22
22
|
require_relative "capybara/lightpanda/element_extension"
|
|
23
23
|
require_relative "capybara/lightpanda/driver"
|
|
24
|
+
require_relative "capybara/lightpanda/railtie" if defined?(Rails::Railtie)
|
|
24
25
|
|
|
25
26
|
module Capybara
|
|
26
27
|
module Lightpanda
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Navid Emad
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- lib/capybara/lightpanda/node.rb
|
|
90
90
|
- lib/capybara/lightpanda/options.rb
|
|
91
91
|
- lib/capybara/lightpanda/process.rb
|
|
92
|
+
- lib/capybara/lightpanda/railtie.rb
|
|
92
93
|
- lib/capybara/lightpanda/tasks/binary.rake
|
|
93
94
|
- lib/capybara/lightpanda/utils/attempt.rb
|
|
94
95
|
- lib/capybara/lightpanda/utils/event.rb
|