capybara-lightpanda 0.5.0 → 0.6.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 +9 -0
- data/lib/capybara/lightpanda/browser.rb +11 -5
- data/lib/capybara/lightpanda/node.rb +92 -9
- data/lib/capybara/lightpanda/process.rb +19 -4
- data/lib/capybara/lightpanda/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad9d7f3b8897578e98e75f2b27f35ec4f2178c4ef884d9b69b79ad8d60378a42
|
|
4
|
+
data.tar.gz: ce602d8d708904f4f2f5dced507f838445fccb6693df8350779cf544c6871fa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05d437473e7f35d10af8b8d65e460f9808e395df438881fdb6eac5e0d8af3cfce36cee4fb156e17803889645411508778c927570987d85fb7512190a7af49dfe
|
|
7
|
+
data.tar.gz: edb94ac9d172c62b61496d3a92f9c408406642c9053517f053ba8829a4c704847f9c9d1903a5b79fb0acdb743aa26f476a092e8b3050b9677d2896fae0e637e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6699 (published 2026-06-11). The driver refuses to start against older binaries.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- File upload through Capybara's standard `attach_file` now works end to end. `attach_file("Avatar", "/path/to/photo.png")` populates the `<input type=file>` and fires `change`, and — the piece that was missing before — the file's bytes are submitted with the form as proper multipart data (filename, `Content-Type`, content). So uploads the server actually receives (avatar pickers, CSV/import forms, ActiveStorage attachments) can be driven and asserted, not just the client-side `change` handler. File paths are read on the machine running Lightpanda.
|
|
10
|
+
- Drag-and-drop file and data upload through Capybara's standard `Element#drop`. `find("#dropzone").drop("/path/to/report.pdf")` reads the file and rebuilds it as a `File` inside the page; `find("#dropzone").drop("text/plain" => "hello")` drops typed string data. The driver builds a `DataTransfer` and fires `dragenter` → `dragover` → `drop`, so HTML5 dropzones (React Dropzone, Uppy, ActiveStorage direct-upload) receive the payload through `event.dataTransfer.files` / `.items`. This is drag-and-drop *upload*, not element-to-element `drag_to`, which still needs the layout geometry Lightpanda doesn't have.
|
|
11
|
+
|
|
3
12
|
## [0.5.0] - 2026-05-22
|
|
4
13
|
|
|
5
14
|
> **Update Lightpanda before upgrading.** Requires a nightly build ≥ 6353 (published 2026-05-21). The driver refuses to start against older binaries.
|
|
@@ -136,11 +136,6 @@ module Capybara
|
|
|
136
136
|
# would otherwise require explicit cookies.clear / storage.clear /
|
|
137
137
|
# close-target dance, and the browser auto-isolates state for the
|
|
138
138
|
# new context. Driver#reset! delegates here.
|
|
139
|
-
#
|
|
140
|
-
# Side benefit: avoids `Page.navigate("about:blank")` against a
|
|
141
|
-
# non-blank tab, which doesn't actually replace the document on
|
|
142
|
-
# current Lightpanda nightly (lightpanda-io/browser#2363). The
|
|
143
|
-
# context-disposal path sidesteps that bug entirely.
|
|
144
139
|
def reset
|
|
145
140
|
dispose_browser_context
|
|
146
141
|
@client.clear_subscriptions
|
|
@@ -508,6 +503,17 @@ module Capybara
|
|
|
508
503
|
page_command("DOM.describeNode", objectId: remote_object_id).dig("node", "backendNodeId")
|
|
509
504
|
end
|
|
510
505
|
|
|
506
|
+
# Populate a file <input> from one or more local file paths via
|
|
507
|
+
# DOM.setFileInputFiles (PR #2635, build ≥6625): Lightpanda resolves the
|
|
508
|
+
# objectId, replaces input.files with a real FileList, and fires
|
|
509
|
+
# `input`/`change`. The submitted form then carries the bytes as
|
|
510
|
+
# multipart/form-data (PR #2654, build ≥6672) — both halves are needed,
|
|
511
|
+
# which is why MINIMUM_NIGHTLY_BUILD sits at the 6672 floor. Paths are
|
|
512
|
+
# read off the machine running Lightpanda (local for the spawned process).
|
|
513
|
+
def set_file_input_files(remote_object_id, paths)
|
|
514
|
+
page_command("DOM.setFileInputFiles", objectId: remote_object_id, files: paths)
|
|
515
|
+
end
|
|
516
|
+
|
|
511
517
|
def screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary)
|
|
512
518
|
params = { format: format.to_s }
|
|
513
519
|
params[:quality] = quality if quality && format == :jpeg
|
|
@@ -113,14 +113,18 @@ module Capybara
|
|
|
113
113
|
call("function() { this.dispatchEvent(new MouseEvent('mouseover', {bubbles: true, cancelable: true})) }")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
116
|
+
# Kept as a deliberate no-op despite upstream now tracking scroll position
|
|
117
|
+
# (`window.scrollTo`/`scrollBy` update `window._scroll_pos`, `Element`
|
|
118
|
+
# exposes `scrollTop`/`scrollLeft` — Window.zig/Element.zig). Wiring it
|
|
119
|
+
# would still misbehave: Lightpanda never clamps to content height
|
|
120
|
+
# (`scrollHeight`/`clientHeight` are a hardcoded 1e8), so `:bottom`/`:center`
|
|
121
|
+
# are meaningless; element scroll is decoupled from window scroll; and with
|
|
122
|
+
# no layout `getBoundingClientRect` isn't scroll-aware, so `scroll_to(el,
|
|
123
|
+
# align:)` can't position anything. So there's nothing meaningful to scroll
|
|
124
|
+
# to. Silently succeed so callers like `session.scroll_to(find('#thing'))`
|
|
125
|
+
# don't crash with NotImplementedError. The `:scroll` capability stays in
|
|
126
|
+
# `capybara_skip`. (Window-position scroll IS reachable for real via
|
|
127
|
+
# `execute_script('window.scrollTo(...)')` if a caller truly needs it.)
|
|
124
128
|
def scroll_to(*); end
|
|
125
129
|
def scroll_by(*); end
|
|
126
130
|
|
|
@@ -147,6 +151,21 @@ module Capybara
|
|
|
147
151
|
end
|
|
148
152
|
end
|
|
149
153
|
|
|
154
|
+
# Capybara's drag-and-drop API (`Element#drop`). String/Pathname arguments
|
|
155
|
+
# are file paths — read here and rebuilt as `File` objects in the page;
|
|
156
|
+
# Hash arguments are `{ mime_type => data }` string drops. We assemble a
|
|
157
|
+
# `DataTransfer` and fire `dragenter` -> `dragover` -> `drop` on this
|
|
158
|
+
# element, so HTML5 dropzones see the payload via `event.dataTransfer`.
|
|
159
|
+
#
|
|
160
|
+
# DataTransfer/DataTransferItem/DragEvent landed upstream in PR #2671
|
|
161
|
+
# (build ≥6699) and are guaranteed by the MINIMUM_NIGHTLY_BUILD floor;
|
|
162
|
+
# without them the drop JS raises "DataTransfer is not defined".
|
|
163
|
+
def drop(*args)
|
|
164
|
+
files, strings = partition_drop_args(args)
|
|
165
|
+
call(DROP_JS, files.to_json, strings.to_json)
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
150
169
|
def select_option
|
|
151
170
|
call(SELECT_OPTION_JS)
|
|
152
171
|
end
|
|
@@ -272,7 +291,13 @@ module Capybara
|
|
|
272
291
|
when "checkbox", "radio"
|
|
273
292
|
call(SET_CHECKBOX_JS, value ? true : false)
|
|
274
293
|
when "file"
|
|
275
|
-
|
|
294
|
+
# DOM.setFileInputFiles (PR #2635, build ≥6625) sets input.files +
|
|
295
|
+
# fires change; multipart form submission carries the bytes upstream
|
|
296
|
+
# (PR #2654, build ≥6672, webapi/net/FormData.zig). Both halves are in
|
|
297
|
+
# the floor, so attach_file uploads end-to-end. `value` is a path
|
|
298
|
+
# String or Array<String> (multiple: true); cast each element so a
|
|
299
|
+
# Pathname / non-string locator still serializes over CDP.
|
|
300
|
+
driver.browser.set_file_input_files(@remote_object_id, Array(value).map(&:to_s))
|
|
276
301
|
when "date"
|
|
277
302
|
call(SET_VALUE_JS, format_date_value(value))
|
|
278
303
|
when "time"
|
|
@@ -349,6 +374,44 @@ module Capybara
|
|
|
349
374
|
n.positive? ? str[0, n] : str
|
|
350
375
|
end
|
|
351
376
|
|
|
377
|
+
# Split `drop` arguments into file descriptors and string-data descriptors.
|
|
378
|
+
# Strings/Pathnames (Capybara normalizes `#to_path`) are file paths, read
|
|
379
|
+
# here and base64-encoded so binary content survives the JSON hop; Hashes
|
|
380
|
+
# are `{ type => data }` string drops. Returns `[files, strings]`.
|
|
381
|
+
def partition_drop_args(args)
|
|
382
|
+
files = []
|
|
383
|
+
strings = []
|
|
384
|
+
args.each do |arg|
|
|
385
|
+
if arg.is_a?(Hash)
|
|
386
|
+
arg.each { |type, data| strings << { type: type.to_s, data: data.to_s } }
|
|
387
|
+
else
|
|
388
|
+
path = arg.to_s
|
|
389
|
+
files << {
|
|
390
|
+
name: File.basename(path),
|
|
391
|
+
type: drop_mime_for(path),
|
|
392
|
+
b64: [File.binread(path)].pack("m0"),
|
|
393
|
+
}
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
[files, strings]
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# The dropzone handler only reads `file.name`, but real upload widgets key
|
|
400
|
+
# off `file.type`, so map the common upload extensions and fall back to a
|
|
401
|
+
# generic binary type.
|
|
402
|
+
DROP_MIME_TYPES = {
|
|
403
|
+
".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".png" => "image/png",
|
|
404
|
+
".gif" => "image/gif", ".webp" => "image/webp", ".svg" => "image/svg+xml",
|
|
405
|
+
".pdf" => "application/pdf", ".txt" => "text/plain", ".csv" => "text/csv",
|
|
406
|
+
".json" => "application/json", ".html" => "text/html", ".xml" => "application/xml",
|
|
407
|
+
".zip" => "application/zip",
|
|
408
|
+
}.freeze
|
|
409
|
+
private_constant :DROP_MIME_TYPES
|
|
410
|
+
|
|
411
|
+
def drop_mime_for(path)
|
|
412
|
+
DROP_MIME_TYPES.fetch(File.extname(path).downcase, "application/octet-stream")
|
|
413
|
+
end
|
|
414
|
+
|
|
352
415
|
# Whitespace-normalized text (Cuprite pattern). Capybara's text matchers compare
|
|
353
416
|
# against this, and Lightpanda's textContent preserves source-template whitespace
|
|
354
417
|
# differently than Chrome — without normalization, multi-line fixtures fail
|
|
@@ -474,6 +537,26 @@ module Capybara
|
|
|
474
537
|
}
|
|
475
538
|
JS
|
|
476
539
|
|
|
540
|
+
# Build a DataTransfer from the JSON payloads and replay the HTML5 drop
|
|
541
|
+
# sequence on this element. Files arrive base64-encoded and are rebuilt
|
|
542
|
+
# with `atob` (Blob accepts the binary string directly); string drops are
|
|
543
|
+
# added as typed items so the page can read them via getData/getAsString.
|
|
544
|
+
DROP_JS = <<~JS
|
|
545
|
+
function(filesJson, stringsJson) {
|
|
546
|
+
var el = this;
|
|
547
|
+
var dt = new DataTransfer();
|
|
548
|
+
JSON.parse(filesJson).forEach(function(f) {
|
|
549
|
+
dt.items.add(new File([atob(f.b64)], f.name, { type: f.type }));
|
|
550
|
+
});
|
|
551
|
+
JSON.parse(stringsJson).forEach(function(s) {
|
|
552
|
+
dt.items.add(s.data, s.type);
|
|
553
|
+
});
|
|
554
|
+
['dragenter', 'dragover', 'drop'].forEach(function(name) {
|
|
555
|
+
el.dispatchEvent(new DragEvent(name, { bubbles: true, cancelable: true, dataTransfer: dt }));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
JS
|
|
559
|
+
|
|
477
560
|
VISIBLE_JS = "function() { return _lightpanda.isVisible(this); }"
|
|
478
561
|
|
|
479
562
|
VISIBLE_TEXT_JS = "function() { return _lightpanda.visibleText(this); }"
|
|
@@ -68,7 +68,15 @@ module Capybara
|
|
|
68
68
|
# unconditionally, so the floor MUST include the build that introduced
|
|
69
69
|
# it; the flag is a fatal UnknownOption on builds < 6353),
|
|
70
70
|
# PR #2498 (StyleManager: author display rule beats UA [hidden] — fixes
|
|
71
|
-
# the Stimulus/Alpine dropdown ElementNotFound)
|
|
71
|
+
# the Stimulus/Alpine dropdown ElementNotFound),
|
|
72
|
+
# PR #2635 (dom: DOM.setFileInputFiles backs input.files + fires change
|
|
73
|
+
# for <input type=file>) AND PR #2654 (forms: encode file inputs as
|
|
74
|
+
# multipart/form-data on submit — filename + Content-Type + bytes per
|
|
75
|
+
# RFC 7578). Both halves are required for attach_file to upload end-to-end:
|
|
76
|
+
# #2635 populates the FileList, #2654 makes form submission carry the
|
|
77
|
+
# bytes. Node#fill_input's `when "file"` branch calls
|
|
78
|
+
# Browser#set_file_input_files, so the floor MUST include the #2654 build;
|
|
79
|
+
# on builds 6625–6671 the file attaches but the form submits empty.
|
|
72
80
|
# NOTE: the gem's teardown hang is the live-CDP-connection SIGTERM hang
|
|
73
81
|
# (limitation #7B) — telemetry-independent, present on 6353 AND on the #2509
|
|
74
82
|
# fix build, handled by the at_exit WS-close plus the SIGKILL backstop
|
|
@@ -76,9 +84,16 @@ module Capybara
|
|
|
76
84
|
# disables telemetry, so it never creates the curl multi #2507 needs. Keep
|
|
77
85
|
# both teardown defenses even after #2511 (the variant-B fix, MERGED in
|
|
78
86
|
# build 6371) lands in a nightly.
|
|
79
|
-
# Build
|
|
80
|
-
#
|
|
81
|
-
|
|
87
|
+
# Build 6672 = the #2654 merge (22d1c5ec, 2026-06-08) — the first commit
|
|
88
|
+
# carrying both file-upload halves.
|
|
89
|
+
# PR #2671 (DataTransfer / DataTransferItem / DataTransferItemList +
|
|
90
|
+
# DragEvent, merged 2026-06-10) provides the APIs Node#drop's DROP_JS
|
|
91
|
+
# assembles its payload from; on builds without it the drop JS raises
|
|
92
|
+
# "DataTransfer is not defined".
|
|
93
|
+
# Build 6699 = the #2671 merge (d1f4c409, 2026-06-10) — now the binding
|
|
94
|
+
# floor. (The prior 6672 file-upload floor — and 6353 before it — are
|
|
95
|
+
# subsumed.)
|
|
96
|
+
MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6699")
|
|
82
97
|
|
|
83
98
|
attr_reader :pid, :ws_url, :version, :nightly_build
|
|
84
99
|
|