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
|
@@ -43,7 +43,7 @@ module Capybara
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def closed?
|
|
46
|
-
@status == :closed
|
|
46
|
+
@status == :closed || @status == :error
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def open?
|
|
@@ -102,10 +102,15 @@ module Capybara
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
@driver.on(:error) do |event|
|
|
105
|
+
# Do NOT raise here. This callback fires synchronously from
|
|
106
|
+
# @driver.parse(data) inside the reader thread, which sets
|
|
107
|
+
# abort_on_exception = true. Raising DeadBrowserError escapes
|
|
108
|
+
# the reader's narrow IO rescue and aborts the entire Ruby
|
|
109
|
+
# process. Mark the connection dead and let Client#command
|
|
110
|
+
# surface DeadBrowserError on its next dispatch via closed?.
|
|
111
|
+
@logger&.puts("✗ WebSocket error: #{event.message}")
|
|
105
112
|
@status = :error
|
|
106
113
|
@messages.close
|
|
107
|
-
|
|
108
|
-
raise DeadBrowserError, "WebSocket error: #{event.message}"
|
|
109
114
|
end
|
|
110
115
|
end
|
|
111
116
|
|
|
@@ -152,10 +157,25 @@ module Capybara
|
|
|
152
157
|
def parse_message(data)
|
|
153
158
|
JSON.parse(data, max_nesting: false)
|
|
154
159
|
rescue JSON::ParserError => e
|
|
155
|
-
|
|
160
|
+
warn_parse_failure(e.message)
|
|
156
161
|
|
|
157
162
|
nil
|
|
158
163
|
end
|
|
164
|
+
|
|
165
|
+
# Dedupe identical parse-failure warnings per WebSocket instance.
|
|
166
|
+
# Lightpanda occasionally emits CDP frames that embed a bare
|
|
167
|
+
# `undefined` token (invalid JSON — see upstream-wishlist.md A41)
|
|
168
|
+
# and a complex page reproduces the same frame on every load,
|
|
169
|
+
# which previously flooded test output with one warn per frame.
|
|
170
|
+
# Surface the first occurrence per unique error so the upstream
|
|
171
|
+
# regression stays visible, then suppress repeats.
|
|
172
|
+
def warn_parse_failure(message)
|
|
173
|
+
@parse_warnings ||= {}
|
|
174
|
+
return if @parse_warnings[message]
|
|
175
|
+
|
|
176
|
+
@parse_warnings[message] = true
|
|
177
|
+
warn "Failed to parse WebSocket message: #{message}"
|
|
178
|
+
end
|
|
159
179
|
end
|
|
160
180
|
end
|
|
161
181
|
end
|
|
@@ -71,6 +71,11 @@ module Capybara
|
|
|
71
71
|
@ws&.close
|
|
72
72
|
@message_thread&.join(1) || @message_thread&.kill
|
|
73
73
|
@subscriber.clear
|
|
74
|
+
# Wake any in-flight callers blocked on `pending.value!(timeout)` so
|
|
75
|
+
# they raise DeadBrowserError immediately (via the `@ws.closed?` check
|
|
76
|
+
# in #command) instead of stalling for the full @options.timeout — a
|
|
77
|
+
# 30s freeze per pending command on a dying browser.
|
|
78
|
+
fail_pending_commands
|
|
74
79
|
@pendings.clear
|
|
75
80
|
end
|
|
76
81
|
|
|
@@ -92,6 +97,14 @@ module Capybara
|
|
|
92
97
|
@mutex.synchronize { @command_id += 1 }
|
|
93
98
|
end
|
|
94
99
|
|
|
100
|
+
# Resolve every pending IVar with nil so blocked callers fall through
|
|
101
|
+
# `pending.value!(timeout)` immediately. `try_set` is a no-op if the
|
|
102
|
+
# IVar already carries a real response (race-safe against an in-flight
|
|
103
|
+
# handle_message that landed just before close).
|
|
104
|
+
def fail_pending_commands
|
|
105
|
+
@pendings.each_value { |ivar| ivar.try_set(nil) }
|
|
106
|
+
end
|
|
107
|
+
|
|
95
108
|
def start_message_thread
|
|
96
109
|
@message_thread = Thread.new do
|
|
97
110
|
Thread.current.abort_on_exception = true
|
|
@@ -77,7 +77,8 @@ module Capybara
|
|
|
77
77
|
find { |cookie| cookie.name == name }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def set(name:, value:, domain: nil, path: "/", secure: false, http_only: false,
|
|
80
|
+
def set(name:, value:, domain: nil, path: "/", secure: false, http_only: false, # rubocop:disable Metrics/ParameterLists
|
|
81
|
+
same_site: nil, expires: nil)
|
|
81
82
|
params = {
|
|
82
83
|
name: name,
|
|
83
84
|
value: value,
|
|
@@ -87,6 +88,10 @@ module Capybara
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
params[:domain] = domain if domain
|
|
91
|
+
# CDP rejects unknown SameSite values; pass through only the canonical
|
|
92
|
+
# spec strings ("Strict" / "Lax" / "None") so YAML noise from a hand-
|
|
93
|
+
# edited file doesn't reach the browser.
|
|
94
|
+
params[:sameSite] = same_site if %w[Strict Lax None].include?(same_site)
|
|
90
95
|
params[:expires] = expires.to_i if expires
|
|
91
96
|
|
|
92
97
|
browser.command("Network.setCookie", **params)
|
|
@@ -123,7 +128,7 @@ module Capybara
|
|
|
123
128
|
|
|
124
129
|
# set() takes keyword args, but YAML round-trips give us a hash with the
|
|
125
130
|
# raw CDP keys (camelCase). Normalize and forward.
|
|
126
|
-
def restore_cookie(hash)
|
|
131
|
+
def restore_cookie(hash) # rubocop:disable Metrics/PerceivedComplexity
|
|
127
132
|
attrs = hash.transform_keys(&:to_s)
|
|
128
133
|
params = {
|
|
129
134
|
name: attrs["name"],
|
|
@@ -133,6 +138,7 @@ module Capybara
|
|
|
133
138
|
http_only: attrs["httpOnly"] || false,
|
|
134
139
|
}
|
|
135
140
|
params[:domain] = attrs["domain"] if attrs["domain"]
|
|
141
|
+
params[:same_site] = attrs["sameSite"] if attrs["sameSite"]
|
|
136
142
|
exp = attrs["expires"]
|
|
137
143
|
params[:expires] = Time.at(exp) if exp.is_a?(Numeric) && exp.positive?
|
|
138
144
|
set(**params)
|
|
@@ -10,7 +10,7 @@ module Capybara
|
|
|
10
10
|
|
|
11
11
|
attr_reader :app, :options
|
|
12
12
|
|
|
13
|
-
delegate %i[current_url title] => :browser
|
|
13
|
+
delegate %i[current_url title status_code response_headers] => :browser
|
|
14
14
|
|
|
15
15
|
def initialize(app, options = {})
|
|
16
16
|
super()
|
|
@@ -31,6 +31,22 @@ module Capybara
|
|
|
31
31
|
false
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Escape hatch to the underlying Browser for callers that need raw CDP
|
|
35
|
+
# access — e.g. Lightpanda's `LP.*` extensions (`getMarkdown`,
|
|
36
|
+
# `getSemanticTree`, `detectForms`, …) that aren't worth exposing through
|
|
37
|
+
# the Capybara DSL. Mirrors `capybara-playwright-driver`'s
|
|
38
|
+
# `with_playwright_page`. Yields the Browser; returns whatever the block
|
|
39
|
+
# returns.
|
|
40
|
+
#
|
|
41
|
+
# driver.with_lightpanda_browser do |browser|
|
|
42
|
+
# browser.page_command("LP.getMarkdown")
|
|
43
|
+
# end
|
|
44
|
+
def with_lightpanda_browser(&block)
|
|
45
|
+
raise ArgumentError, "block must be given" unless block
|
|
46
|
+
|
|
47
|
+
block.call(browser)
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
def visit(url)
|
|
35
51
|
@started = true
|
|
36
52
|
browser.go_to(url)
|
|
@@ -189,8 +205,11 @@ module Capybara
|
|
|
189
205
|
|
|
190
206
|
def save_screenshot(path, **_options)
|
|
191
207
|
browser.screenshot(path: path)
|
|
192
|
-
rescue BinaryError, BinaryNotFoundError
|
|
193
|
-
# Browser can't start (
|
|
208
|
+
rescue BinaryError, BinaryNotFoundError, BrowserError, TimeoutError
|
|
209
|
+
# Browser can't start (version too old), is already dead (DeadBrowserError),
|
|
210
|
+
# the CDP call timed out, or returned any other CDP-level error. Teardown
|
|
211
|
+
# screenshots are best-effort — swallow so the real test failure surfaces
|
|
212
|
+
# instead of a "browser already gone" stack trace.
|
|
194
213
|
nil
|
|
195
214
|
end
|
|
196
215
|
|
|
@@ -221,12 +240,15 @@ module Capybara
|
|
|
221
240
|
end
|
|
222
241
|
|
|
223
242
|
# Expanded error list for Capybara retry logic (Cuprite pattern).
|
|
243
|
+
# MouseEventFailed is in Cuprite's list, but Lightpanda has no
|
|
244
|
+
# rendering engine and the gem dispatches clicks through JS — the
|
|
245
|
+
# underlying CDP Input.dispatchMouseEvent path doesn't run, so
|
|
246
|
+
# MouseEventFailed is never raised.
|
|
224
247
|
def invalid_element_errors
|
|
225
248
|
[
|
|
226
249
|
NodeNotFoundError,
|
|
227
250
|
NoExecutionContextError,
|
|
228
251
|
ObsoleteNode,
|
|
229
|
-
MouseEventFailed,
|
|
230
252
|
]
|
|
231
253
|
end
|
|
232
254
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Escape hatch onto Capybara::Node::Element so users can reach our driver-level
|
|
4
|
+
# Node (and its remote_object_id, `call`, etc.) without going through
|
|
5
|
+
# `element.base`. Mirrors capybara-playwright-driver's
|
|
6
|
+
# `with_playwright_element_handle` (lib/capybara/playwright/node.rb). The is_a?
|
|
7
|
+
# guard keeps the patch safe when both this gem and another Capybara driver
|
|
8
|
+
# are loaded in the same process.
|
|
9
|
+
module Capybara
|
|
10
|
+
module Lightpanda
|
|
11
|
+
module ElementExtension
|
|
12
|
+
def with_lightpanda_node(&block)
|
|
13
|
+
raise ArgumentError, "block must be given" unless block
|
|
14
|
+
raise "#{base.inspect} is not a Capybara::Lightpanda::Node" unless base.is_a?(Capybara::Lightpanda::Node)
|
|
15
|
+
|
|
16
|
+
block.call(base)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
Capybara::Node::Element.prepend(Capybara::Lightpanda::ElementExtension)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Lightpanda
|
|
5
|
+
# Hash subclass that downcases the lookup key. CDP returns response headers
|
|
6
|
+
# with lowercased names ("content-type"), but Capybara callers reach for
|
|
7
|
+
# the canonical casing ("Content-Type"). Mirrors capybara-playwright-driver's
|
|
8
|
+
# Headers class (lib/capybara/playwright/page.rb).
|
|
9
|
+
class Headers < Hash
|
|
10
|
+
def [](key)
|
|
11
|
+
super(key.to_s.downcase)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|