capybara-lightpanda 0.9.0 → 0.11.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.
@@ -97,11 +97,136 @@ module Capybara
97
97
  # merged 2026-06-12) made the index.js readystatechange re-dispatch
98
98
  # shim redundant, so it was removed — on builds without #2708 Turbo's
99
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")
100
+ # Build 6736 carried the #2708 merge.
101
+ # PR #2722 (Browser.setDownloadBehavior streams Content-Disposition:
102
+ # attachment responses to disk under downloadPath + emits
103
+ # Browser.downloadWillBegin / downloadProgress, merged 2026-06-19) backs
104
+ # the Downloads tracker (downloads.rb) wired in Browser#create_page. On
105
+ # builds < 7545 setDownloadBehavior is a bare success no-op — every
106
+ # download would silently never be written to disk — so the floor MUST
107
+ # include it. Note Lightpanda triggers downloads on Content-Disposition:
108
+ # attachment, NOT by MIME type (a text/csv response without that header
109
+ # is rendered as a normal navigation), which is why Capybara's
110
+ # MIME-triggered :download shared spec stays in capybara_skip.
111
+ # Build 7545 = the #2722 merge (a808386c).
112
+ # PR #2785 (build 7550, improve innerText + outerText) AND PR #2795
113
+ # (build 7571, innerText uses StyleManager visibility) make native
114
+ # Element.innerText implement the HTML rendered-text collection steps —
115
+ # block line breaks + skipping display:none descendants. The
116
+ # _lightpanda.visibleText predicate (predicates.js) now delegates the
117
+ # rendered-text collection to native innerText (keeping only a visibility
118
+ # gate + ShadowRoot fragment walk), so the floor MUST include #2795; on
119
+ # builds < 7571 innerText returns textContent verbatim (no block breaks,
120
+ # leaks display:none text).
121
+ # Build 7571 = the #2795 merge (48ed689c).
122
+ # PR #2719 (css: apply @layer block rules to the cascade, merged
123
+ # 2026-07-17) makes StyleManager rank `@layer` rules correctly. Before it,
124
+ # a rule inside an `@layer` block was dropped from the cascade entirely,
125
+ # so an element hidden by an `@layer` `display: none` — the default shape
126
+ # of Tailwind v4's generated CSS — reported `checkVisibility() === true`.
127
+ # `_lightpanda.isVisible` (predicates.js) terminates in checkVisibility(),
128
+ # so on builds < 8160 Capybara believes such elements are visible and acts
129
+ # on them: clicks land on hidden nodes and `assert_no_selector` passes for
130
+ # visible ones. That is a wrong-answer bug rather than an exception, which
131
+ # is exactly the kind users can't diagnose, so the floor MUST include it.
132
+ # Build 8160 = the #2719 merge (b19b5725).
133
+ # PR #2983 (css: respect @layer priority, merged 2026-07-24) is the second
134
+ # half of that same fix and the reason 8160 is NOT sufficient. #2719 only
135
+ # made rules inside an `@layer` block *participate* in the cascade; it
136
+ # ranked them by specificity + document order alone. #2983 widened
137
+ # VisibilityRule.priority to carry a 12-bit layer rank, so layer order
138
+ # finally beats specificity as the spec requires. On builds 8160–8280 a
139
+ # page whose layers disagree about `display` resolves to the wrong rule —
140
+ # Tailwind v4's `@layer utilities` losing to `@layer base` is the common
141
+ # shape — and `_lightpanda.isVisible` (predicates.js) terminates in
142
+ # checkVisibility(), so Capybara again believes hidden elements are
143
+ # visible. Same undiagnosable wrong-answer class as #2719 itself, so the
144
+ # floor MUST include it.
145
+ # Build 8281 = the #2983 merge (76e2e7e6).
146
+ # PR #3015 (cdp: sanitize non-UTF-8 values, merged 2026-07-24) fixes
147
+ # issue #2992: a legacy-encoded Content-Disposition filename made
148
+ # Browser.downloadWillBegin emit `suggestedFilename` as a JSON array of
149
+ # bytes instead of a string. Downloads#build_will_handler does
150
+ # File.basename(params["suggestedFilename"].to_s), so below this build it
151
+ # records a "[130, 160, ...]" basename and Driver#downloads hands back a
152
+ # path that does not exist.
153
+ # Build 8283 = the #3015 merge (8fbcc7e5).
154
+ # PR #3054 (forms: close the ancestor dialog on method=dialog submission,
155
+ # merged 2026-07-25) makes `<form method="dialog">` close its nearest
156
+ # ancestor <dialog>, set returnValue from the submitter's IDL value, fire
157
+ # close, and perform no navigation. Below it the submission falls through
158
+ # to a GET navigation and the dialog stays open forever — which is the
159
+ # <dialog>+Turbo-confirm idiom Spree 5's admin uses for every destroy
160
+ # confirmation. There is no gem-side workaround to guard this: the only
161
+ # defense is the floor.
162
+ # Build 8311 = the #3054 merge (c917cadc).
163
+ # PR #3058 (forms: include optgroup children in a select's list of
164
+ # options, merged 2026-07-26) makes `<option>`s inside an `<optgroup>`
165
+ # reachable through HTMLSelectElement (options / value / selectedIndex /
166
+ # submission). Below it Capybara's `select` cannot find a grouped option
167
+ # at all and there is no gem-side workaround — a plain ElementNotFound on
168
+ # any grouped <select>. Build 8328 = the #3058 merge (c7182354).
169
+ # PR #3080 (webapi: increase max timer count, merged 2026-07-29) lifts the
170
+ # one-shot setTimeout cap 512 → 2048 (Airbnb-class pages schedule 600+);
171
+ # below it timers past the cap are silently dropped and page JS stalls in
172
+ # ways that read as Capybara timeouts. Build 8412 = the #3080 merge.
173
+ # PR #3081 + #3082/#3085 (script load/error events; dynamic scripts whose
174
+ # `src` is set via setAttribute, merged 2026-07-30) — below them an
175
+ # inline script fired no load event, a throwing script still fired
176
+ # `load`, and a setAttribute('src') script never loaded, which breaks
177
+ # loader idioms (importmap shims, lazy widget bootstraps) that gate the
178
+ # UI Capybara waits for. Builds 8414/8419.
179
+ # PR #3087 (fix: potential UAF when an option's value is programmatically
180
+ # set, merged 2026-07-30) — a use-after-free in exactly the path
181
+ # `Node#select_option`/`Node#set` on a <select> drives, i.e. a browser
182
+ # crash surfacing as DeadBrowserError mid-spec.
183
+ # Build 8448 = the #3087 merge (6d824c88) — the binding floor of the
184
+ # 2026-07-30 bump.
185
+ # Subsumed by 8448, recorded so they are not re-derived: build 8298 made
186
+ # Network.enable idempotent (the gem never hit it — the Notification is
187
+ # per-BrowserContext and Network#enable's @enabled guard means one enable
188
+ # per context), and build 8305 (#3048) derives scrollWidth/scrollHeight
189
+ # from element content (the gem keeps scroll as a no-op either way).
190
+ # PR #2664 (Emulation.setDeviceMetricsOverride, build 7556) is subsumed:
191
+ # Browser#set_viewport calls it on every create_page to honor the
192
+ # `window_size` option. Note the override only reached
193
+ # Page.getLayoutMetrics later (~build 8300); the gem does not depend on
194
+ # that half, so it is NOT part of the floor.
195
+ #
196
+ # 2026-08-25 bump 8448 -> 8796: #3257 (HTMLElement.draggable IDL, build
197
+ # 8793) and #3259 (MouseEvent coordinate getters floor to integers
198
+ # Chrome-style, build 8796; PointerEvent stays fractional). The floor
199
+ # guarantees native `.draggable`, so the drag scripts in node.rb read it
200
+ # verbatim — the `_lightpanda.isDraggable` polyfill was retired with
201
+ # this bump — and drag events carry integer clientX/Y (the shared spec
202
+ # asserting that runs un-skipped). Build 8796 = the #3259 merge
203
+ # (341a01570), which is also the 2026-08-25 nightly cut.
204
+ MINIMUM_NIGHTLY_BUILD = Gem::Version.new("8796")
205
+
206
+ # Second, equivalent floor for the *release* channel.
207
+ #
208
+ # Tagged releases are built with `-Dversion=<tag>`, which resolves to a
209
+ # plain semver with no pre-release tag — so `lightpanda version` prints a
210
+ # bare "0.3.6" carrying no commit counter at all, and MINIMUM_NIGHTLY_BUILD
211
+ # has nothing to compare against. Releases are cut from the same trunk, so
212
+ # a release is acceptable exactly when its own commit count clears the
213
+ # nightly floor. As of 2026-08-25 NO published release clears the 8796
214
+ # floor — 0.3.7 (build 8671) predates the draggable-IDL and
215
+ # coordinate-flooring fixes the floor exists for — so this pin names the
216
+ # NEXT release. 0.3.8 is not tagged yet; any future tag is cut from a
217
+ # trunk already past build 8834, so it clears the floor by construction.
218
+ # Until upstream tags it, the release channel is deliberately unusable
219
+ # (nightly-only), reported with the standard too-old error. When 0.3.8
220
+ # ships: verify `git rev-list --count 0.3.8` >= 8796 and update the
221
+ # lockstep table in process_test.rb.
222
+ #
223
+ # INVARIANT: every MINIMUM_NIGHTLY_BUILD bump must also move this to the
224
+ # first release containing that build (`git rev-list --count <tag>` in the
225
+ # browser repo tells you). Leaving it behind would let the release channel
226
+ # silently accept a binary the nightly channel rejects.
227
+ MINIMUM_RELEASE = Gem::Version.new("0.3.8")
103
228
 
104
- attr_reader :pid, :ws_url, :version, :nightly_build
229
+ attr_reader :pid, :ws_url, :version, :nightly_build, :release
105
230
 
106
231
  def initialize(options)
107
232
  @options = options
@@ -109,6 +234,7 @@ module Capybara
109
234
  @ws_url = nil
110
235
  @version = nil
111
236
  @nightly_build = nil
237
+ @release = nil
112
238
  @stdout_r = nil
113
239
  @stdout_w = nil
114
240
  @stderr_r = nil
@@ -147,20 +273,37 @@ module Capybara
147
273
 
148
274
  private
149
275
 
276
+ # `lightpanda version` prints one of two shapes, and the gem supports both
277
+ # so a suite can either track nightly or pin a reproducible release:
278
+ #
279
+ # "1.0.0-nightly.8285+de85a51d" rolling nightly (also "dev.NNNN" for a
280
+ # locally compiled tree — same
281
+ # `git rev-list --count HEAD` counter,
282
+ # different label). Checked against
283
+ # MINIMUM_NIGHTLY_BUILD.
284
+ # "0.3.5" a tagged release: bare semver, no build
285
+ # metadata. Checked against MINIMUM_RELEASE.
286
+ #
287
+ # The release form is matched anchored — a bare semver and nothing else —
288
+ # so a stray "1.2.3" inside some other string can never be mistaken for a
289
+ # release. Anything that matches neither shape stays a hard failure: a
290
+ # binary we cannot identify is never assumed to be new enough.
150
291
  def check_minimum_version(binary_path)
151
292
  stdout, = Open3.capture3(binary_path, "version")
152
293
  @version = stdout.strip
153
- # Accept either `nightly.NNNN` (publicly distributed builds) or
154
- # `dev.NNNN` (locally compiled trees) — the build number is the same
155
- # `git rev-list --count HEAD` counter, just labelled differently.
156
- build = @version[/(?:nightly|dev)\.(\d+)/, 1]
157
- @nightly_build = Gem::Version.new(build) if build
158
294
 
159
- return if @nightly_build && @nightly_build >= MINIMUM_NIGHTLY_BUILD
295
+ if (build = @version[/(?:nightly|dev)\.(\d+)/, 1])
296
+ @nightly_build = Gem::Version.new(build)
297
+ return if @nightly_build >= MINIMUM_NIGHTLY_BUILD
298
+ elsif (tag = @version[/\A\d+\.\d+\.\d+\z/])
299
+ @release = Gem::Version.new(tag)
300
+ return if @release >= MINIMUM_RELEASE
301
+ end
160
302
 
161
303
  raise BinaryError,
162
304
  "Lightpanda #{@version} is too old. " \
163
- "This gem requires build >= #{MINIMUM_NIGHTLY_BUILD}. " \
305
+ "This gem requires nightly build >= #{MINIMUM_NIGHTLY_BUILD} " \
306
+ "or release >= #{MINIMUM_RELEASE}. " \
164
307
  "Update: #{Binary.update_hint(binary_path)}"
165
308
  rescue Errno::ENOENT
166
309
  # Binary not runnable — let attempt_start handle it
@@ -230,7 +373,26 @@ module Capybara
230
373
  # floor). Always on so linked CSS contributes to checkVisibility /
231
374
  # getComputedStyle; see .claude/rules/lightpanda-io.md limitation #6.
232
375
  "--enable-external-stylesheets",
376
+ # Raise the inbound CDP WebSocket message cap from Lightpanda's 1 MiB
377
+ # default (Config.zig `cdp_max_message_size`) to 100 MiB, matching
378
+ # Chrome's inbound DevTools buffer. An inbound message over the cap is
379
+ # dropped with WS close 1009 and NO JSON-RPC error — the whole CDP
380
+ # connection dies (wishlist A44). The 1 MiB default already covers
381
+ # axe-core (~553 KB), but larger injected bundles (jQuery + plugins,
382
+ # instrumentation) via execute_script exceed it (Node#drop used to as
383
+ # well, before files moved to DOM.setFileInputFiles). The reader
384
+ # buffer grows lazily (16 KB at init),
385
+ # so a high cap costs nothing until a big message actually arrives.
386
+ # Flag added in PR #2760 (build 7441) — below the floor, so guaranteed.
387
+ "--cdp-max-message-size",
388
+ (100 * 1024 * 1024).to_s,
233
389
  ]
390
+ # Opt-in image fetching (upstream #3230, build >= 8834). Deliberately
391
+ # NOT always-on like --enable-external-stylesheets: images never feed
392
+ # the DOM predicates, so the default spends no bandwidth on them. On
393
+ # builds < 8834 the flag is a fatal UnknownOption at boot — see
394
+ # Options#load_images.
395
+ base.push("--load-resources", "image") if @options.load_images
234
396
  extra = ENV.fetch("LIGHTPANDA_EXTRA_ARGS", "").split
235
397
  base + extra
236
398
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Lightpanda
5
- VERSION = "0.9.0"
5
+ VERSION = "0.11.0"
6
6
  end
7
7
  end
@@ -14,6 +14,7 @@ require_relative "capybara/lightpanda/utils/wait"
14
14
  require_relative "capybara/lightpanda/client"
15
15
  require_relative "capybara/lightpanda/headers"
16
16
  require_relative "capybara/lightpanda/network"
17
+ require_relative "capybara/lightpanda/downloads"
17
18
  require_relative "capybara/lightpanda/cookies"
18
19
  require_relative "capybara/lightpanda/keyboard"
19
20
  require_relative "capybara/lightpanda/browser"
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.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Navid Emad
@@ -79,16 +79,19 @@ files:
79
79
  - lib/capybara/lightpanda/browser/modals.rb
80
80
  - lib/capybara/lightpanda/browser/navigation.rb
81
81
  - lib/capybara/lightpanda/browser/runtime.rb
82
+ - lib/capybara/lightpanda/browser/selenium_compat.rb
82
83
  - lib/capybara/lightpanda/client.rb
83
84
  - lib/capybara/lightpanda/client/subscriber.rb
84
85
  - lib/capybara/lightpanda/client/web_socket.rb
85
86
  - lib/capybara/lightpanda/cookies.rb
87
+ - lib/capybara/lightpanda/downloads.rb
86
88
  - lib/capybara/lightpanda/driver.rb
87
89
  - lib/capybara/lightpanda/element_extension.rb
88
90
  - lib/capybara/lightpanda/errors.rb
89
91
  - lib/capybara/lightpanda/headers.rb
90
92
  - lib/capybara/lightpanda/javascripts/attach.js
91
93
  - lib/capybara/lightpanda/javascripts/banner.js
94
+ - lib/capybara/lightpanda/javascripts/errors.js
92
95
  - lib/capybara/lightpanda/javascripts/predicates.js
93
96
  - lib/capybara/lightpanda/javascripts/turbo.js
94
97
  - lib/capybara/lightpanda/keyboard.rb
@@ -126,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
129
  - !ruby/object:Gem::Version
127
130
  version: '0'
128
131
  requirements: []
129
- rubygems_version: 4.0.10
132
+ rubygems_version: 4.0.16
130
133
  specification_version: 4
131
134
  summary: Capybara driver for the Lightpanda headless browser
132
135
  test_files: []