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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d2922897678cf6ea18bd630b63703ae59ca213b4909e5c42d60e5c1b55e7faa
4
- data.tar.gz: 8b01db5b4f0b7c8da5e13d2c9b157c2184463e3b22637ec1d6d5d4d3d84d17f7
3
+ metadata.gz: ad9d7f3b8897578e98e75f2b27f35ec4f2178c4ef884d9b69b79ad8d60378a42
4
+ data.tar.gz: ce602d8d708904f4f2f5dced507f838445fccb6693df8350779cf544c6871fa4
5
5
  SHA512:
6
- metadata.gz: 7e09df8530d946074c1b9022820ebe3ff6c3bc19d69ee1f543df7c3e65c6d6cc2ace6c65910028fb69ca7253488a88df10f3c271bcced82358e71e44c128cb1e
7
- data.tar.gz: 93feae0c267585dfc745d01ec65d651b682903794588ea2cc18c3c071b313c4b869bde6c35cd8bfbe8c2e00b1c51ac49ba1cd6ddc5b624c33f1f0e7dc8575286
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
- # Lightpanda has no rendering engine `window.scrollTo` / `scrollIntoView`
117
- # are no-ops at the browser level, and `getBoundingClientRect` reflects
118
- # logical-DOM geometry rather than scroll-aware viewport coords. So
119
- # there's nothing to scroll. Silently succeed so callers like
120
- # `session.scroll_to(find('#thing'))` (Selenium-flavoured specs leaning
121
- # on real layout) don't crash with NotImplementedError; assertions that
122
- # depend on post-scroll visibility are already gated by the cuprite
123
- # fallback in dual-driver setups.
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
- raise NotImplementedError, "File uploads not yet supported by Lightpanda"
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 6353 = main HEAD merge f1b0adf9 (2026-05-20) carrying #2487 +
80
- # #2498; the first published nightly with it is the 2026-05-21 cut.
81
- MINIMUM_NIGHTLY_BUILD = Gem::Version.new("6353")
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Lightpanda
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Navid Emad