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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -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 +209 -308
- 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 -43
- data/lib/capybara/lightpanda/keyboard.rb +18 -1
- data/lib/capybara/lightpanda/network.rb +50 -21
- data/lib/capybara/lightpanda/node.rb +63 -24
- data/lib/capybara/lightpanda/process.rb +63 -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 +6 -4
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0295a232795b3fee642523097324529b501ca520427691642949f59815794bb9'
|
|
4
|
+
data.tar.gz: 8469cceee8e75e2d1f12f6be8528c2d80081aabb096aa0deb84bae9a2467d7b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f233ff2cb0f5d1f6c146c1a417360ee7f2ef630f0d82bade3afd13176ee940629254cca842ea04762d174f00a595c7950a40ceb88cc4026dc3708b90675e3750
|
|
7
|
+
data.tar.gz: '0748a7f1786f2eded7256a9d4106ead5da15abf5be28966e4c5525f600b6a9000f11bc878b2697c1f85e315d524cc5569938c38bf2c12c6aa53bb24f7ed3c2b8'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-17
|
|
4
|
+
|
|
5
|
+
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6269 (published 2026-05-16 or later). The driver refuses to start against older binaries.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Driver#status_code` and `Driver#response_headers` — read the HTTP status code and response headers from the most recent document navigation. Lookups on `response_headers` are case-insensitive (`response_headers["Content-Type"]` works against any header casing). Caveat: calling `driver.network.disable` also disables the navigation-response handler — they share a CDP toggle.
|
|
10
|
+
- `Driver#with_lightpanda_browser { |browser| … }` and `element.with_lightpanda_node { |node| … }` — escape hatches for tests that need raw access to Lightpanda's CDP client and `LP.*` extensions. Mixed-driver setups are safe: the element helper no-ops against non-Lightpanda elements.
|
|
11
|
+
- Shifted-symbol keyboard input — `send_keys([:shift, "1"])` now produces `"!"`, `[:shift, "2"]` produces `"@"`, etc., matching a US keyboard layout. Letters fall through to their existing upcase path unchanged.
|
|
12
|
+
- `LIGHTPANDA_EXTRA_ARGS` env var — whitespace-split tokens get appended to the spawned `lightpanda serve` command line, so you can experiment with opt-in upstream flags (e.g. `LIGHTPANDA_EXTRA_ARGS="--log_format pretty"`) without forking the gem.
|
|
13
|
+
- `Binary.configure { |b| b.required_version = "…"; b.cache_time = …; b.install_dir = "…"; b.proxy_addr = … }`, plus `Binary.update`, `Binary.remove`, `Binary.current_version`, `Binary.install_path` — explicit, scriptable binary management. Pair with the new `rake lightpanda:binary:{version, update[version], remove}` tasks if you want to download the binary once before parallel workers start.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Inline `<style> @media` rules and `window.matchMedia(q).matches` now evaluate against the fixed 1920×1080 viewport, so visibility checks on mobile-vs-desktop CTAs gated by **inline** `@media` queries no longer surface both variants. External `<link rel="stylesheet">` responsive CSS is still out of scope (browser limitation — see README).
|
|
18
|
+
- `network.headers = { … }`, `network.add_headers(…)`, and `network.clear_headers` now enable the Network domain on first call. No need to flip `network.enable` separately.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A WebSocket-level error (bad frame, oversized message) no longer tears down the Ruby process. Pending CDP commands now fail immediately on disconnect instead of blocking for the full 30 s timeout, so teardown after a browser crash no longer freezes the suite.
|
|
23
|
+
- `Cookies#load` now restores the `SameSite` attribute. Auth-cookie flows that rely on `SameSite=Strict` / `Lax` survive a YAML round-trip through `cookies.store` / `cookies.load`.
|
|
24
|
+
- Stale (detached) element references now raise `ObsoleteNode` from every node operation, not just `text` / `all_text` / `visible_text`. Capybara's automatic-reload re-runs the original query instead of silently reading stale data.
|
|
25
|
+
- A browser crash followed by reconnect now wipes session state (modal subscriptions, frame stack). Subsequent tests no longer see "no dialog fired" or stale iframe handles.
|
|
26
|
+
- `save_screenshot` from Rails' `before_teardown` after a failed system test no longer masks the original failure with a "browser already gone" stack trace.
|
|
27
|
+
- `find_modal` of the wrong type now reports the message of whatever other dialog actually fired — so an `alert` firing where the test expected a `confirm` shows up clearly in the failure message.
|
|
28
|
+
- A `find(...)` immediately after a click that triggers navigation no longer raises `NoExecutionContextError`.
|
|
29
|
+
- A broken or permission-denied `lsof` now surfaces as a `BinaryError` pointing at the underlying problem instead of silently failing port reclamation.
|
|
30
|
+
- `session.scroll_to(node)` no longer raises `NotImplementedError`. Lightpanda still has no rendering so the call does nothing, but callers who didn't care about the result no longer crash.
|
|
31
|
+
- `network.traffic` reads (used by `wait_for_network_idle`) are now thread-safe — no more inconsistent counts when the CDP message thread is busy.
|
|
32
|
+
- Several internal browser-side handle leaks plugged — long shared-spec sessions no longer accumulate orphaned object references.
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
36
|
+
- The last JavaScript polyfill bundle (form-IDL accessors like `form.enctype` and submitter `formAction` / `formMethod`) is gone — Lightpanda implements these natively now. No code change required on your end.
|
|
37
|
+
- The iframe-context retry workaround and the `HTMLDialogElement.show` / `showModal` / `close` polyfill — both implemented natively upstream. No code change required.
|
|
38
|
+
|
|
39
|
+
### Internal
|
|
40
|
+
|
|
41
|
+
- `rake spec:shared:parallel` — the Capybara shared-spec battery now runs in parallel across N worker processes (~3.7× at 4 workers, ~6.6× at 8). Default worker count is `Etc.nprocessors / 2`; override with `RSPEC_WORKERS=N`.
|
|
42
|
+
|
|
3
43
|
## [0.3.0] - 2026-05-12
|
|
4
44
|
|
|
5
45
|
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6109 (published 2026-05-12). The driver refuses to start against older binaries.
|
data/README.md
CHANGED
|
@@ -40,6 +40,9 @@ Capybara.javascript_driver = :lightpanda
|
|
|
40
40
|
> [!TIP]
|
|
41
41
|
> The Lightpanda binary is auto-downloaded on first use — no separate install step needed.
|
|
42
42
|
|
|
43
|
+
> [!IMPORTANT]
|
|
44
|
+
> Lightpanda is a headless agentic browser, not a layout engine. By design it does **not** fetch external `<link rel="stylesheet">`, evaluate `@media` rules, or implement `window.matchMedia()`. Any spec whose visibility depends on responsive CSS — for example a mobile/desktop CTA pair hidden via `@media (min-width: …)` — should stay on Cuprite (or whichever full-browser driver you were already using). The [dual-driver setup](https://navidemad.github.io/capybara-lightpanda/#docs) routes the layout-sensitive minority to Cuprite and the structural majority to Lightpanda for speed.
|
|
45
|
+
|
|
43
46
|
## Credits
|
|
44
47
|
|
|
45
48
|
- [Lightpanda](https://lightpanda.io/) — the headless browser
|
|
@@ -23,7 +23,7 @@ module Capybara
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
GITHUB_RELEASE_URL = "https://github.com/lightpanda-io/browser/releases/download
|
|
26
|
+
GITHUB_RELEASE_URL = "https://github.com/lightpanda-io/browser/releases/download"
|
|
27
27
|
|
|
28
28
|
PLATFORMS = {
|
|
29
29
|
%w[x86_64 linux] => "lightpanda-x86_64-linux",
|
|
@@ -31,23 +31,90 @@ module Capybara
|
|
|
31
31
|
%w[arm64 darwin] => "lightpanda-aarch64-macos",
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
|
+
DEFAULT_CACHE_TIME = 86_400
|
|
35
|
+
|
|
34
36
|
class << self
|
|
37
|
+
# Set a specific release tag (e.g. "0.3.0") to pin downloads to that
|
|
38
|
+
# release. When nil, the rolling "nightly" tag is used. The pin only
|
|
39
|
+
# affects download URL construction — the gem's MINIMUM_NIGHTLY_BUILD
|
|
40
|
+
# floor is still enforced at process start.
|
|
41
|
+
attr_accessor :required_version
|
|
42
|
+
|
|
43
|
+
# Seconds before a cached unpinned binary is re-downloaded. Pinned
|
|
44
|
+
# binaries are never refreshed on age (they're pinned).
|
|
45
|
+
attr_writer :cache_time, :install_dir, :logger
|
|
46
|
+
attr_accessor :proxy_addr, :proxy_port, :proxy_user, :proxy_pass
|
|
47
|
+
|
|
48
|
+
def cache_time
|
|
49
|
+
@cache_time ||= Integer(ENV.fetch("LIGHTPANDA_CACHE_TIME", DEFAULT_CACHE_TIME))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def install_dir
|
|
53
|
+
@install_dir ||= File.dirname(default_binary_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def logger
|
|
57
|
+
return @logger if defined?(@logger) && @logger
|
|
58
|
+
return nil unless ENV["LIGHTPANDA_DEBUG"]
|
|
59
|
+
|
|
60
|
+
@logger = Capybara::Lightpanda::Logger.new($stderr.tap { |s| s.sync = true })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def configure
|
|
64
|
+
yield self
|
|
65
|
+
end
|
|
66
|
+
|
|
35
67
|
def path
|
|
36
|
-
@path ||=
|
|
68
|
+
@path ||= update
|
|
37
69
|
end
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
# Canonical entrypoint: ensure the binary at install_path is current,
|
|
72
|
+
# download if needed, return its path. Pinned (required_version set)
|
|
73
|
+
# never re-downloads when present. Unpinned re-downloads when older
|
|
74
|
+
# than cache_time.
|
|
75
|
+
def update
|
|
76
|
+
destination = install_path
|
|
77
|
+
|
|
78
|
+
if required_version
|
|
79
|
+
if File.executable?(destination)
|
|
80
|
+
log("Pinned #{required_version} present at #{destination}")
|
|
81
|
+
return destination
|
|
82
|
+
end
|
|
83
|
+
elsif cached_fresh?(destination)
|
|
84
|
+
log("Cached binary at #{destination} is fresh (< #{cache_time}s)")
|
|
85
|
+
return destination
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
download
|
|
41
89
|
end
|
|
42
90
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
def
|
|
46
|
-
path =
|
|
47
|
-
|
|
91
|
+
# Delete the cached binary. Returns the path that was deleted, or nil
|
|
92
|
+
# if nothing was there.
|
|
93
|
+
def remove
|
|
94
|
+
path = install_path
|
|
95
|
+
unless File.exist?(path)
|
|
96
|
+
log("Nothing to remove at #{path}")
|
|
97
|
+
return nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
File.delete(path)
|
|
101
|
+
@path = nil
|
|
102
|
+
log("Removed #{path}")
|
|
48
103
|
path
|
|
49
104
|
end
|
|
50
105
|
|
|
106
|
+
# Returns the `lightpanda version` output of the cached binary, or nil
|
|
107
|
+
# if the binary isn't present / not runnable.
|
|
108
|
+
def current_version
|
|
109
|
+
path = install_path
|
|
110
|
+
return nil unless File.executable?(path)
|
|
111
|
+
|
|
112
|
+
stdout, _, status = Open3.capture3(path, "version")
|
|
113
|
+
status.success? ? stdout.strip : nil
|
|
114
|
+
rescue Errno::ENOENT
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
51
118
|
def run(*)
|
|
52
119
|
stdout, stderr, status = Open3.capture3(path, *)
|
|
53
120
|
|
|
@@ -72,28 +139,18 @@ module Capybara
|
|
|
72
139
|
result.output.strip
|
|
73
140
|
end
|
|
74
141
|
|
|
75
|
-
def find
|
|
76
|
-
env_path = ENV.fetch("LIGHTPANDA_BIN", nil)
|
|
77
|
-
return env_path if env_path && File.executable?(env_path)
|
|
78
|
-
|
|
79
|
-
path_binary = find_in_path
|
|
80
|
-
return path_binary if path_binary
|
|
81
|
-
|
|
82
|
-
default_path = default_binary_path
|
|
83
|
-
return default_path if File.executable?(default_path)
|
|
84
|
-
|
|
85
|
-
nil
|
|
86
|
-
end
|
|
87
|
-
|
|
88
142
|
def download
|
|
89
143
|
binary_name = platform_binary
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
tag = required_version || "nightly"
|
|
145
|
+
url = "#{GITHUB_RELEASE_URL}/#{tag}/#{binary_name}"
|
|
146
|
+
destination = install_path
|
|
92
147
|
|
|
148
|
+
log("Downloading #{binary_name} (#{tag}) → #{destination}")
|
|
93
149
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
94
150
|
|
|
95
151
|
download_file(url, destination)
|
|
96
152
|
FileUtils.chmod(0o755, destination)
|
|
153
|
+
@path = destination
|
|
97
154
|
|
|
98
155
|
destination
|
|
99
156
|
end
|
|
@@ -111,35 +168,23 @@ module Capybara
|
|
|
111
168
|
File.join(cache_dir, "lightpanda", "lightpanda")
|
|
112
169
|
end
|
|
113
170
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
# Path the gem writes the downloaded binary to. Honors a
|
|
172
|
+
# user-configured install_dir; otherwise falls back to default_binary_path.
|
|
173
|
+
def install_path
|
|
174
|
+
if @install_dir
|
|
175
|
+
File.join(@install_dir, "lightpanda")
|
|
176
|
+
else
|
|
177
|
+
default_binary_path
|
|
121
178
|
end
|
|
122
|
-
|
|
123
|
-
nil
|
|
124
179
|
end
|
|
125
180
|
|
|
126
|
-
|
|
127
|
-
header = File.binread(path, 4)
|
|
128
|
-
|
|
129
|
-
return true if elf_binary?(header)
|
|
130
|
-
return true if mach_o_binary?(header)
|
|
131
|
-
|
|
132
|
-
false
|
|
133
|
-
rescue StandardError
|
|
134
|
-
false
|
|
135
|
-
end
|
|
181
|
+
private
|
|
136
182
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
def cached_fresh?(path)
|
|
184
|
+
return false unless File.executable?(path)
|
|
185
|
+
return true if cache_time.zero?
|
|
140
186
|
|
|
141
|
-
|
|
142
|
-
header.start_with?("\xCF\xFA\xED\xFE")
|
|
187
|
+
(Time.now - File.mtime(path)) < cache_time
|
|
143
188
|
end
|
|
144
189
|
|
|
145
190
|
def normalize_arch(arch)
|
|
@@ -167,7 +212,7 @@ module Capybara
|
|
|
167
212
|
def follow_redirects(uri, destination, limit = 10)
|
|
168
213
|
raise BinaryNotFoundError, "Too many redirects" if limit.zero?
|
|
169
214
|
|
|
170
|
-
|
|
215
|
+
http_start(uri) do |http|
|
|
171
216
|
request = Net::HTTP::Get.new(uri)
|
|
172
217
|
|
|
173
218
|
http.request(request) do |response|
|
|
@@ -177,6 +222,7 @@ module Capybara
|
|
|
177
222
|
response.read_body { |chunk| file.write(chunk) }
|
|
178
223
|
end
|
|
179
224
|
when Net::HTTPRedirection
|
|
225
|
+
log("Redirected → #{response['location']}")
|
|
180
226
|
follow_redirects(URI.parse(response["location"]), destination, limit - 1)
|
|
181
227
|
else
|
|
182
228
|
raise BinaryNotFoundError, "Failed to download binary: #{response.code} #{response.message}"
|
|
@@ -184,6 +230,22 @@ module Capybara
|
|
|
184
230
|
end
|
|
185
231
|
end
|
|
186
232
|
end
|
|
233
|
+
|
|
234
|
+
def http_start(uri, &)
|
|
235
|
+
if proxy_addr
|
|
236
|
+
Net::HTTP.start(
|
|
237
|
+
uri.host, uri.port,
|
|
238
|
+
proxy_addr, proxy_port, proxy_user, proxy_pass,
|
|
239
|
+
use_ssl: uri.scheme == "https", &
|
|
240
|
+
)
|
|
241
|
+
else
|
|
242
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", &)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def log(message)
|
|
247
|
+
logger&.puts("[lightpanda binary] #{message}")
|
|
248
|
+
end
|
|
187
249
|
end
|
|
188
250
|
end
|
|
189
251
|
end
|