capybara-lightpanda 0.14.1 → 0.15.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 +33 -0
- data/README.md +4 -4
- data/lib/capybara/lightpanda/auto_scripts.rb +3 -3
- data/lib/capybara/lightpanda/browser/console.rb +73 -51
- data/lib/capybara/lightpanda/browser/finder.rb +83 -32
- data/lib/capybara/lightpanda/browser/modals.rb +8 -7
- data/lib/capybara/lightpanda/browser/navigation.rb +10 -1
- data/lib/capybara/lightpanda/browser/runtime.rb +3 -3
- data/lib/capybara/lightpanda/browser.rb +13 -4
- data/lib/capybara/lightpanda/client/web_socket.rb +63 -5
- data/lib/capybara/lightpanda/client.rb +7 -4
- data/lib/capybara/lightpanda/cookies.rb +1 -1
- data/lib/capybara/lightpanda/driver.rb +27 -28
- data/lib/capybara/lightpanda/errors.rb +3 -3
- data/lib/capybara/lightpanda/javascripts/attach.js +2 -1
- data/lib/capybara/lightpanda/javascripts/predicates.js +73 -8
- data/lib/capybara/lightpanda/keyboard.rb +15 -5
- data/lib/capybara/lightpanda/network.rb +1 -1
- data/lib/capybara/lightpanda/node.rb +96 -53
- data/lib/capybara/lightpanda/options.rb +9 -7
- data/lib/capybara/lightpanda/process.rb +71 -6
- data/lib/capybara/lightpanda/version.rb +1 -1
- metadata +1 -2
- data/lib/capybara/lightpanda/javascripts/errors.js +0 -74
|
@@ -11,14 +11,22 @@ module Capybara
|
|
|
11
11
|
# `frame_scoped`: the node lives in an iframe's document — it was found
|
|
12
12
|
# while a frame was switched to, or from another frame-scoped node. See
|
|
13
13
|
# #wrap_with_attached_guard for what that changes.
|
|
14
|
-
def initialize(driver, remote_object_id, frame_scoped: false)
|
|
15
|
-
super(driver, remote_object_id)
|
|
14
|
+
def initialize(driver, remote_object_id, initial_cache = {}, frame_scoped: false)
|
|
15
|
+
super(driver, remote_object_id, initial_cache)
|
|
16
16
|
@remote_object_id = remote_object_id
|
|
17
17
|
@frame_scoped = frame_scoped
|
|
18
|
+
@tag_name = initial_cache[:tag_name]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Capybara's finder hints → the spec `_lightpanda.findHints` answers.
|
|
22
|
+
# Only tag_name is cached (when the bounded helper runs); Capybara reads
|
|
23
|
+
# initial_cache[:tag_name] before asking the node.
|
|
24
|
+
def self.hint_spec(texts: nil, **)
|
|
25
|
+
texts&.any? ? { texts: texts.map(&:to_s) } : {}
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
# Capybara::Driver::Node#native returns the constructor's second argument,
|
|
21
|
-
# which here is the raw CDP objectId — a String. So the Selenium
|
|
29
|
+
# which here is the raw CDP objectId — a String. So the Selenium
|
|
22
30
|
# idiom `element.native.send_keys(...)` (solidus's
|
|
23
31
|
# return_authorizations_spec.rb does exactly that) died with
|
|
24
32
|
# "undefined method 'send_keys' for an instance of String". This driver
|
|
@@ -112,7 +120,7 @@ module Capybara
|
|
|
112
120
|
self.class.new(driver, result["objectId"])
|
|
113
121
|
end
|
|
114
122
|
|
|
115
|
-
# Smart property/attribute getter
|
|
123
|
+
# Smart property/attribute getter.
|
|
116
124
|
# Returns resolved URLs for src/href, raw attributes otherwise.
|
|
117
125
|
def [](name)
|
|
118
126
|
call(PROPERTY_OR_ATTRIBUTE_JS, name.to_s)
|
|
@@ -165,10 +173,10 @@ module Capybara
|
|
|
165
173
|
def scroll_to(*); end
|
|
166
174
|
def scroll_by(*); end
|
|
167
175
|
|
|
168
|
-
# Dispatch an arbitrary DOM event by name.
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
176
|
+
# Dispatch an arbitrary DOM event by name. Picks the right Event
|
|
177
|
+
# constructor for known mouse/focus/form names and falls back to a
|
|
178
|
+
# generic Event for everything else (so callers can fire custom events
|
|
179
|
+
# like `node.trigger('lp:custom')`).
|
|
172
180
|
def trigger(event)
|
|
173
181
|
call(TRIGGER_JS, event.to_s)
|
|
174
182
|
end
|
|
@@ -194,10 +202,10 @@ module Capybara
|
|
|
194
202
|
# on this element, so HTML5 dropzones see the payload via
|
|
195
203
|
# `event.dataTransfer`.
|
|
196
204
|
#
|
|
197
|
-
# Files reach the page
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
205
|
+
# Files reach the page through a hidden `<input type=file>`: it is
|
|
206
|
+
# attached to this element's document, `DOM.setFileInputFiles` points it
|
|
207
|
+
# at the paths (the browser reads the bytes off disk itself), and the
|
|
208
|
+
# drop JS moves `input.files` into the
|
|
201
209
|
# DataTransfer and removes the input. Previously the bytes were base64'd
|
|
202
210
|
# into the `Runtime.callFunctionOn` message, which capped a drop at
|
|
203
211
|
# ~70 MB under `--cdp-max-message-size` and pinned every byte in Ruby;
|
|
@@ -216,15 +224,15 @@ module Capybara
|
|
|
216
224
|
end
|
|
217
225
|
|
|
218
226
|
# Maps Capybara's documented drop_modifiers aliases onto the DragEvent
|
|
219
|
-
# init keys (`ctrlKey`, `metaKey`, ...).
|
|
227
|
+
# init keys (`ctrlKey`, `metaKey`, ...).
|
|
220
228
|
DRAG_MODIFIER_ALIASES = { control: :ctrl, command: :meta, cmd: :meta }.freeze
|
|
221
229
|
|
|
222
230
|
# Capybara's `Element#drag_to` — HTML5 half only. HTML5_DRAG_JS replays
|
|
223
|
-
# Capybara's own Selenium HTML5_DRAG_DROP_SCRIPT
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
231
|
+
# Capybara's own Selenium HTML5_DRAG_DROP_SCRIPT: dragstart on the
|
|
232
|
+
# draggable ancestor, then dragenter -> 2x dragover -> drop (accepted)
|
|
233
|
+
# or dragleave (refused) -> dragend, setTimeout-paced, sharing one
|
|
234
|
+
# DataTransfer so `setData` in the page's dragstart handler is readable
|
|
235
|
+
# at drop. Runs through `evaluate_async` (the script signals
|
|
228
236
|
# completion via the appended callback), so the drag has fully played out
|
|
229
237
|
# before this method returns.
|
|
230
238
|
#
|
|
@@ -235,8 +243,8 @@ module Capybara
|
|
|
235
243
|
# Selenium presses a real button, then apply the same
|
|
236
244
|
# prevented-or-no-draggable-ancestor test.
|
|
237
245
|
#
|
|
238
|
-
# `steps:`/`scroll:` (
|
|
239
|
-
# ignored so suites migrating from
|
|
246
|
+
# `steps:`/`scroll:` (legacy mouse-path knobs of other CDP drivers) are
|
|
247
|
+
# accepted and ignored so suites migrating from them don't ArgumentError.
|
|
240
248
|
def drag_to(other, html5: nil, delay: 0.05, drop_modifiers: [], **)
|
|
241
249
|
keys = Array(drop_modifiers).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym).to_s }
|
|
242
250
|
html5 = !call(LEGACY_DRAG_CHECK_JS) if html5.nil?
|
|
@@ -318,20 +326,18 @@ module Capybara
|
|
|
318
326
|
end
|
|
319
327
|
|
|
320
328
|
# Ancestor chain from `parentNode` up to (but not including) `document`,
|
|
321
|
-
# returned as Lightpanda::Node wrappers.
|
|
329
|
+
# returned as Lightpanda::Node wrappers.
|
|
322
330
|
def parents
|
|
323
331
|
oids = driver.browser.parents_of(@remote_object_id)
|
|
324
332
|
oids.map { |oid| self.class.new(driver, oid, frame_scoped: @frame_scoped) }
|
|
325
333
|
end
|
|
326
334
|
|
|
327
|
-
def find_xpath(selector)
|
|
328
|
-
|
|
329
|
-
object_ids.map { |oid| self.class.new(driver, oid, frame_scoped: @frame_scoped) }
|
|
335
|
+
def find_xpath(selector, **hints)
|
|
336
|
+
find_hinted("xpath", selector, hints)
|
|
330
337
|
end
|
|
331
338
|
|
|
332
|
-
def find_css(selector)
|
|
333
|
-
|
|
334
|
-
object_ids.map { |oid| self.class.new(driver, oid, frame_scoped: @frame_scoped) }
|
|
339
|
+
def find_css(selector, **hints)
|
|
340
|
+
find_hinted("css", selector, hints)
|
|
335
341
|
end
|
|
336
342
|
|
|
337
343
|
# Equality compares the underlying DOM node via backendNodeId, the only
|
|
@@ -368,6 +374,11 @@ module Capybara
|
|
|
368
374
|
|
|
369
375
|
private
|
|
370
376
|
|
|
377
|
+
def find_hinted(method, selector, hints)
|
|
378
|
+
driver.browser.find_within_hinted(@remote_object_id, method, selector, self.class.hint_spec(**hints))
|
|
379
|
+
.map { |oid, cache| self.class.new(driver, oid, cache, frame_scoped: @frame_scoped) }
|
|
380
|
+
end
|
|
381
|
+
|
|
371
382
|
def implicit_submit
|
|
372
383
|
call(IMPLICIT_SUBMIT_JS)
|
|
373
384
|
driver.browser.wait_for_idle
|
|
@@ -446,8 +457,8 @@ module Capybara
|
|
|
446
457
|
|
|
447
458
|
# ISO 8601 week-of-year, "%G" giving the ISO week-numbering year so that
|
|
448
459
|
# the last days of December that belong to week 1 of the next year are
|
|
449
|
-
# rendered with the correct year. Matches
|
|
450
|
-
#
|
|
460
|
+
# rendered with the correct year. Matches what the user would type into
|
|
461
|
+
# a `<input type=week>` field.
|
|
451
462
|
def format_week_value(value)
|
|
452
463
|
return value.to_s if value.is_a?(String) || !value.respond_to?(:to_date)
|
|
453
464
|
|
|
@@ -492,7 +503,7 @@ module Capybara
|
|
|
492
503
|
self.class.new(driver, oid)
|
|
493
504
|
end
|
|
494
505
|
|
|
495
|
-
# Whitespace-normalized text
|
|
506
|
+
# Whitespace-normalized text. Capybara's text matchers compare
|
|
496
507
|
# against this, and Lightpanda's textContent preserves source-template whitespace
|
|
497
508
|
# differently than Chrome — without normalization, multi-line fixtures fail
|
|
498
509
|
# `text: "Line\nLine"` matchers.
|
|
@@ -638,8 +649,15 @@ module Capybara
|
|
|
638
649
|
}
|
|
639
650
|
hit.dispatchEvent(new EventCtor('mouseup', { bubbles: true, cancelable: true }));
|
|
640
651
|
var clickEvt = new EventCtor('click', { bubbles: true, cancelable: true });
|
|
652
|
+
// A link to a scheme the browser can't fetch (mailto:, tel:, sms:…)
|
|
653
|
+
// goes to an external app in Chrome and the page stays. The link's
|
|
654
|
+
// own activation is a no-op for it since upstream #3940 (10536, in
|
|
655
|
+
// the floor); don't navigate to it ourselves either.
|
|
656
|
+
var link = hit.closest ? hit.closest('a[href]') : null;
|
|
657
|
+
var scheme = link && /^\\s*([a-z][a-z0-9+.-]*):/i.exec(link.getAttribute('href'));
|
|
658
|
+
var external = !!scheme && !/^(https?|about|blob|data|file|javascript)$/i.test(scheme[1]);
|
|
641
659
|
var notCancelled = hit.dispatchEvent(clickEvt);
|
|
642
|
-
if (!notCancelled || clickEvt.defaultPrevented) return;
|
|
660
|
+
if (external || !notCancelled || clickEvt.defaultPrevented) return;
|
|
643
661
|
var tag = hit.tagName;
|
|
644
662
|
// A `javascript:` link never navigates: the click's own activation
|
|
645
663
|
// behavior has already run its code. Assigning it to location.href
|
|
@@ -676,9 +694,9 @@ module Capybara
|
|
|
676
694
|
}
|
|
677
695
|
JS
|
|
678
696
|
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
697
|
+
# Picks the right Event constructor based on the event name so listeners
|
|
698
|
+
# that key on `event instanceof MouseEvent` / `instanceof SubmitEvent`
|
|
699
|
+
# see what they expect; everything else goes
|
|
682
700
|
# through a generic Event so custom names ("turbo:load", "lp:custom")
|
|
683
701
|
# still dispatch. Each constructor is feature-detected (`typeof X !==
|
|
684
702
|
# 'undefined'`) before use so a missing IDL on Lightpanda falls back
|
|
@@ -696,6 +714,9 @@ module Capybara
|
|
|
696
714
|
event = new FocusEvent(name, init);
|
|
697
715
|
} else if (name === 'submit' && typeof SubmitEvent !== 'undefined') {
|
|
698
716
|
event = new SubmitEvent(name, init);
|
|
717
|
+
} else if (name === 'input' && typeof InputEvent !== 'undefined') {
|
|
718
|
+
// inputType is what typing sends; a plain Event leaves it undefined.
|
|
719
|
+
event = new InputEvent(name, { bubbles: true, cancelable: false, inputType: 'insertText' });
|
|
699
720
|
} else {
|
|
700
721
|
event = new Event(name, init);
|
|
701
722
|
}
|
|
@@ -757,19 +778,45 @@ module Capybara
|
|
|
757
778
|
}
|
|
758
779
|
JS
|
|
759
780
|
|
|
760
|
-
# Ported
|
|
781
|
+
# Ported from Capybara's Selenium driver
|
|
761
782
|
# (capybara/selenium/extensions/html5_drag.rb, HTML5_DRAG_DROP_SCRIPT) —
|
|
762
|
-
#
|
|
763
|
-
#
|
|
764
|
-
#
|
|
765
|
-
#
|
|
783
|
+
# kept close to ease future syncs. Unlike the original, a drag ends the
|
|
784
|
+
# way it does in a browser: `drop` if the target accepted it (cancelled
|
|
785
|
+
# the last dragover), otherwise `dragleave`, never both. Selenium's
|
|
786
|
+
# script fired `dragleave` before `drop`, and Pragmatic drag and drop
|
|
787
|
+
# reads a relatedTarget-less `dragleave` as the drag leaving the window,
|
|
788
|
+
# so the drop landed on nothing. Two port bugs are fixed too:
|
|
789
|
+
# pointOnRect read `rectPt.top` (a DOMPoint has none, so a target
|
|
790
|
+
# entered from above or below got the source's center as its entry
|
|
791
|
+
# point), and `key` leaked as a global. Cuprite made the same changes
|
|
792
|
+
# (rubycdp/cuprite#343). The
|
|
766
793
|
# `source.draggable` reads are native — the HTMLElement.draggable IDL
|
|
767
794
|
# shipped upstream in #3257 (build 8793, guaranteed by the floor; the
|
|
768
795
|
# `_lightpanda.isDraggable` polyfill was retired with that bump).
|
|
769
796
|
# Coordinates come from getBoundingClientRect, which Lightpanda
|
|
770
797
|
# synthesizes without layout — dropzones reading clientX/Y get
|
|
771
798
|
# plausible-but-synthetic (integer since #3259) numbers.
|
|
799
|
+
#
|
|
800
|
+
# One deliberate deviation: the target is centered by
|
|
801
|
+
# `scrollCenterIntoView`, not `scrollIntoView({block: 'center'})`.
|
|
802
|
+
# Lightpanda's scrollIntoView ignores its options and aligns the
|
|
803
|
+
# element's top with the viewport's, and since upstream #3658 (nightly
|
|
804
|
+
# 10661) rects follow the scroll, so a source just above the target
|
|
805
|
+
# left the viewport and the first dragover carried a negative clientY.
|
|
806
|
+
# The helper does what Chrome does (element center at viewport center,
|
|
807
|
+
# clamped to the scrollable range), measuring against the root's rect
|
|
808
|
+
# so it holds whether or not the build's rects follow the scroll.
|
|
809
|
+
# Pinned by `drag_test.rb`.
|
|
772
810
|
HTML5_DRAG_JS = <<~JS
|
|
811
|
+
function scrollCenterIntoView(el) {
|
|
812
|
+
var doc = el.ownerDocument, win = doc.defaultView;
|
|
813
|
+
var rect = el.getBoundingClientRect();
|
|
814
|
+
var root = doc.documentElement.getBoundingClientRect();
|
|
815
|
+
var x = rect.left - root.left + rect.width / 2 - win.innerWidth / 2;
|
|
816
|
+
var y = rect.top - root.top + rect.height / 2 - win.innerHeight / 2;
|
|
817
|
+
win.scrollTo(Math.max(0, x), Math.max(0, y));
|
|
818
|
+
}
|
|
819
|
+
|
|
773
820
|
function rectCenter(rect){
|
|
774
821
|
return new DOMPoint(
|
|
775
822
|
(rect.left + rect.right)/2,
|
|
@@ -794,7 +841,7 @@ module Capybara
|
|
|
794
841
|
}
|
|
795
842
|
|
|
796
843
|
if (pt.y <= rectPt.y) { // top side
|
|
797
|
-
var minYx = (
|
|
844
|
+
var minYx = (rect.top - pt.y) / slope + pt.x;
|
|
798
845
|
if (rect.left <= minYx && minYx <= rect.right)
|
|
799
846
|
return new DOMPoint(minYx, rect.top);
|
|
800
847
|
}
|
|
@@ -809,12 +856,12 @@ module Capybara
|
|
|
809
856
|
}
|
|
810
857
|
|
|
811
858
|
function dragEnterTarget() {
|
|
812
|
-
target
|
|
859
|
+
scrollCenterIntoView(target);
|
|
813
860
|
var targetRect = target.getBoundingClientRect();
|
|
814
861
|
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
|
815
862
|
|
|
816
863
|
for (var i = 0; i < drop_modifier_keys.length; i++) {
|
|
817
|
-
key = drop_modifier_keys[i];
|
|
864
|
+
var key = drop_modifier_keys[i];
|
|
818
865
|
if (key == "control"){
|
|
819
866
|
key = "ctrl"
|
|
820
867
|
}
|
|
@@ -837,18 +884,14 @@ module Capybara
|
|
|
837
884
|
var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
|
838
885
|
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
839
886
|
target.dispatchEvent(dragOverEvent);
|
|
840
|
-
window.setTimeout(
|
|
887
|
+
window.setTimeout(dragEnd, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
|
|
841
888
|
}
|
|
842
889
|
|
|
843
|
-
function
|
|
844
|
-
var
|
|
845
|
-
var
|
|
846
|
-
target.dispatchEvent(
|
|
847
|
-
|
|
848
|
-
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
|
849
|
-
target.dispatchEvent(dropEvent);
|
|
850
|
-
}
|
|
851
|
-
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
|
890
|
+
function dragEnd(drop, dragOverOpts) {
|
|
891
|
+
var dragEndOptions = Object.assign({}, opts, dragOverOpts);
|
|
892
|
+
var dropOrLeaveEvent = new DragEvent(drop ? 'drop' : 'dragleave', dragEndOptions);
|
|
893
|
+
target.dispatchEvent(dropOrLeaveEvent);
|
|
894
|
+
var dragEndEvent = new DragEvent('dragend', dragEndOptions);
|
|
852
895
|
source.dispatchEvent(dragEndEvent);
|
|
853
896
|
callback.call(true);
|
|
854
897
|
}
|
|
@@ -1066,7 +1109,7 @@ module Capybara
|
|
|
1066
1109
|
# same node (`node #path returns xpath which points to itself`), so this
|
|
1067
1110
|
# emits `/HTML/BODY/DIV[2]/P[1]` rather than the CSS-ish
|
|
1068
1111
|
# `html > body > div:nth-of-type(2) > p` it used to. Chrome exposes no
|
|
1069
|
-
# native equivalent either
|
|
1112
|
+
# native equivalent either.
|
|
1070
1113
|
#
|
|
1071
1114
|
# Every step is indexed, including single children: `P[1]` and `P` select
|
|
1072
1115
|
# the same node, but the explicit index keeps the output stable if a
|
|
@@ -20,10 +20,11 @@ module Capybara
|
|
|
20
20
|
# `Capybara::Lightpanda.configure { |c| c.port = 9222 }` when external
|
|
21
21
|
# tooling needs a known address.
|
|
22
22
|
DEFAULT_PORT = 0
|
|
23
|
-
# Mirrors Lightpanda's own `Viewport.default` (1920x1080)
|
|
24
|
-
#
|
|
25
|
-
# a 1024x768 default would silently shrink the viewport
|
|
26
|
-
# suite and flip `@media` branches under them.
|
|
23
|
+
# Mirrors Lightpanda's own `Viewport.default` (1920x1080), not the
|
|
24
|
+
# 1024x768 some drivers default to. window_size is applied for real now
|
|
25
|
+
# (see below), so a 1024x768 default would silently shrink the viewport
|
|
26
|
+
# of every existing suite and flip `@media` branches under them.
|
|
27
|
+
# Matching the browser's
|
|
27
28
|
# native default keeps `window_size` truthful AND leaves default
|
|
28
29
|
# behavior byte-identical to before it was wired up.
|
|
29
30
|
DEFAULT_WINDOW_SIZE = [1920, 1080].freeze
|
|
@@ -36,16 +37,17 @@ module Capybara
|
|
|
36
37
|
# no rendering engine, so getBoundingClientRect stays synthetic and
|
|
37
38
|
# nothing reflows. Sizing down will not make an off-viewport element
|
|
38
39
|
# report as obscured.
|
|
39
|
-
# headless is accepted
|
|
40
|
+
# headless is accepted so suites migrating from other drivers keep
|
|
41
|
+
# working, but is inert —
|
|
40
42
|
# headless is the only mode Lightpanda runs in.
|
|
41
|
-
# save_path: directory for downloaded files
|
|
43
|
+
# save_path: directory for downloaded files. nil falls
|
|
42
44
|
# back to Capybara.save_path at create_page time; downloads stay off when
|
|
43
45
|
# both are nil (Browser#create_page only opts in when a path exists).
|
|
44
46
|
# raise_on_unhandled_modal: a JS dialog that opens with no
|
|
45
47
|
# accept_modal/dismiss_modal pre-arm in flight is resolved by Lightpanda's
|
|
46
48
|
# silent default (confirm → cancel, prompt → null, alert → dismissed), so
|
|
47
49
|
# a runaway confirm cancels the action and the spec can still pass. false
|
|
48
|
-
# (default
|
|
50
|
+
# (default) warns on stderr; true raises
|
|
49
51
|
# UnhandledModalError from the action that opened it.
|
|
50
52
|
# load_images: adds `image` to the `--load-resources` value so `<img>`
|
|
51
53
|
# elements are fetched over the network (upstream #3230, build >= 8834,
|
|
@@ -264,7 +264,48 @@ module Capybara
|
|
|
264
264
|
# end in Network.loadingFailed, which Network consumes); #3529 (9877)
|
|
265
265
|
# SameSite=None without Secure refused (Cookies#set reports it);
|
|
266
266
|
# #3702 (9968) click no longer focuses (CLICK_JS focuses itself).
|
|
267
|
-
|
|
267
|
+
#
|
|
268
|
+
# 2026-10-16 bump 9994 -> 10641 (= release 1.0.1, the new lockstep cap).
|
|
269
|
+
# Retired or ungated by it:
|
|
270
|
+
# #3958 (10613, ours) Window and Document carry the GlobalEventHandlers
|
|
271
|
+
# `on*` attributes, so javascripts/event_handlers.js is gone — jQuery
|
|
272
|
+
# 1.x sees `'onchange' in window` natively.
|
|
273
|
+
# #3940 (10536) a link to a non-fetchable scheme (mailto:, tel:) is a
|
|
274
|
+
# no-op that keeps the document, so CLICK_JS no longer cancels the
|
|
275
|
+
# click's default from a window listener.
|
|
276
|
+
# #3755 / #3849 (10049 / 10310, ours / upstream) exceptions from timers,
|
|
277
|
+
# rAF, inline handlers and microtasks reach `window.onerror`, so
|
|
278
|
+
# Browser#page_errors sees them; #3757 (10052) hides a closed
|
|
279
|
+
# [popover]; #3759 (10057) routes send_keys into an iframe; #3874
|
|
280
|
+
# (10366) Ctrl+A selects a field — upstream_fixes_test.rb runs them
|
|
281
|
+
# un-gated.
|
|
282
|
+
# #3763 (10239, ours) history traverses pushState entries in place;
|
|
283
|
+
# #3831 (10255) `window` serializes as CyclicObject, not a node;
|
|
284
|
+
# #3843 (10261) a failed navigation commits an error document — their
|
|
285
|
+
# tests drop the nightly_build branches.
|
|
286
|
+
# #3952 (10575) typed text fires `change` when committed by blur/Enter.
|
|
287
|
+
# A `nightly_build` gate is nil on a release binary, so every such test
|
|
288
|
+
# skipped on 1.0.1 although it carried the fix: ungate, don't re-gate.
|
|
289
|
+
#
|
|
290
|
+
# 2026-10-23 bump 10641 -> 10965 (= release 1.0.2, the new lockstep cap;
|
|
291
|
+
# CSS nesting #3962 at 10976 and blank-page timers #4148 at 11012 stay
|
|
292
|
+
# above it). Rides along:
|
|
293
|
+
# #4096 (10904) Runtime.exceptionThrown for unhandled rejections, and
|
|
294
|
+
# `unhandledrejection` no longer fires for a rejection handled in
|
|
295
|
+
# the same tick — errors.js stops reporting those false positives;
|
|
296
|
+
# page_errors_test.rb drops its nightly_build gate. With #4115
|
|
297
|
+
# (10910, an iframe's exception carries its context id) this
|
|
298
|
+
# unblocks the B19 swap off errors.js.
|
|
299
|
+
# #3997 (10685) the scheduler rework: a MessageChannel task loop no
|
|
300
|
+
# longer delays a pending fetch tenfold (#4042), which held
|
|
301
|
+
# wait_for_idle.
|
|
302
|
+
# #3658 (10657) client rects follow the scroll and
|
|
303
|
+
# DOM.scrollIntoViewIfNeeded is real — HTML5_DRAG_JS centers the
|
|
304
|
+
# drop target itself because scrollIntoView ignores `block`.
|
|
305
|
+
MINIMUM_NIGHTLY_BUILD = Gem::Version.new("10965")
|
|
306
|
+
|
|
307
|
+
# Lines of browser output #output keeps (ferrum keeps the same 100).
|
|
308
|
+
OUTPUT_LINES = 100
|
|
268
309
|
|
|
269
310
|
# Second, equivalent floor for the *release* channel.
|
|
270
311
|
#
|
|
@@ -273,16 +314,16 @@ module Capybara
|
|
|
273
314
|
# bare "0.3.6" carrying no commit counter at all, and MINIMUM_NIGHTLY_BUILD
|
|
274
315
|
# has nothing to compare against. Releases are cut from the same trunk, so
|
|
275
316
|
# a release is acceptable exactly when its own commit count clears the
|
|
276
|
-
# nightly floor. 1.0.
|
|
317
|
+
# nightly floor. 1.0.2 (2026-10-23) is build 10965 — exactly the nightly
|
|
277
318
|
# floor, which is no accident: the floor was raised *to* the release so
|
|
278
|
-
# both channels gate the same commit. (0.
|
|
279
|
-
#
|
|
319
|
+
# both channels gate the same commit. (1.0.1 = 10641 predates fixes the
|
|
320
|
+
# floor exists for.)
|
|
280
321
|
#
|
|
281
322
|
# INVARIANT: every MINIMUM_NIGHTLY_BUILD bump must also move this to the
|
|
282
323
|
# first release containing that build (`git rev-list --count <tag>` in the
|
|
283
324
|
# browser repo tells you). Leaving it behind would let the release channel
|
|
284
325
|
# silently accept a binary the nightly channel rejects.
|
|
285
|
-
MINIMUM_RELEASE = Gem::Version.new("1.0.
|
|
326
|
+
MINIMUM_RELEASE = Gem::Version.new("1.0.2")
|
|
286
327
|
|
|
287
328
|
class << self
|
|
288
329
|
# `lightpanda version` prints one of two shapes, and the gem supports
|
|
@@ -348,6 +389,17 @@ module Capybara
|
|
|
348
389
|
@stderr_r = nil
|
|
349
390
|
@stderr_w = nil
|
|
350
391
|
@finalizer_registered = false
|
|
392
|
+
@output = []
|
|
393
|
+
@output_partial = {}
|
|
394
|
+
@output_mutex = Mutex.new
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# The last OUTPUT_LINES lines Lightpanda wrote to stdout/stderr, oldest
|
|
398
|
+
# first, across restarts of this Process. Kept for diagnostics: a crash
|
|
399
|
+
# or panic only ever shows up there, and from build 10783 so do page
|
|
400
|
+
# errors (`page_error` scope, WARN). Mirrors ferrum's Process::Output.
|
|
401
|
+
def output
|
|
402
|
+
@output_mutex.synchronize { @output.dup }
|
|
351
403
|
end
|
|
352
404
|
|
|
353
405
|
def start
|
|
@@ -416,7 +468,7 @@ module Capybara
|
|
|
416
468
|
next unless ready
|
|
417
469
|
|
|
418
470
|
ready[0].each do |io|
|
|
419
|
-
io.read_nonblock(4096)
|
|
471
|
+
record_output(io, io.read_nonblock(4096))
|
|
420
472
|
rescue IO::WaitReadable
|
|
421
473
|
# No data
|
|
422
474
|
rescue EOFError
|
|
@@ -430,6 +482,18 @@ module Capybara
|
|
|
430
482
|
end
|
|
431
483
|
end
|
|
432
484
|
|
|
485
|
+
# Split per pipe so a line cut across two reads is kept whole, and a
|
|
486
|
+
# stdout fragment never glues onto a stderr one.
|
|
487
|
+
def record_output(io, chunk)
|
|
488
|
+
text = chunk.dup.force_encoding(Encoding::UTF_8).scrub
|
|
489
|
+
@output_mutex.synchronize do
|
|
490
|
+
*lines, rest = "#{@output_partial[io]}#{text}".split("\n", -1)
|
|
491
|
+
@output_partial[io] = rest
|
|
492
|
+
@output.concat(lines)
|
|
493
|
+
@output.shift(@output.size - OUTPUT_LINES) if @output.size > OUTPUT_LINES
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
433
497
|
def spawn_process(binary_path)
|
|
434
498
|
args = build_args
|
|
435
499
|
|
|
@@ -499,6 +563,7 @@ module Capybara
|
|
|
499
563
|
ready[0].each do |io|
|
|
500
564
|
chunk = io.read_nonblock(1024)
|
|
501
565
|
output << chunk
|
|
566
|
+
record_output(io, chunk)
|
|
502
567
|
|
|
503
568
|
if (match = output.match(READY_PATTERN))
|
|
504
569
|
@ws_url = "ws://#{match[1]}/"
|
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.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Navid Emad
|
|
@@ -91,7 +91,6 @@ files:
|
|
|
91
91
|
- lib/capybara/lightpanda/headers.rb
|
|
92
92
|
- lib/capybara/lightpanda/javascripts/attach.js
|
|
93
93
|
- lib/capybara/lightpanda/javascripts/banner.js
|
|
94
|
-
- lib/capybara/lightpanda/javascripts/errors.js
|
|
95
94
|
- lib/capybara/lightpanda/javascripts/predicates.js
|
|
96
95
|
- lib/capybara/lightpanda/javascripts/turbo.js
|
|
97
96
|
- lib/capybara/lightpanda/keyboard.rb
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// --- Uncaught page errors ---
|
|
2
|
-
// Lightpanda implements no `Runtime.exceptionThrown` (upstream gap, wishlist
|
|
3
|
-
// B19), so an uncaught exception in page JS reaches no CDP client at all. It is
|
|
4
|
-
// invisible to console_logs, which only ever sees explicit console.* calls, so
|
|
5
|
-
// a handler that dies on a TypeError leaves no trace and the failure surfaces
|
|
6
|
-
// far away as ElementNotFound on whatever it was supposed to produce. That is
|
|
7
|
-
// the exact shape of the solidus taxon-tree failure in real-apps causes.yml.
|
|
8
|
-
//
|
|
9
|
-
// These listeners are passive — no preventDefault, so nothing the page would
|
|
10
|
-
// otherwise see is swallowed, and nothing that would have propagated stops
|
|
11
|
-
// propagating. They re-emit through the same console.debug sentinel channel
|
|
12
|
-
// turbo.js uses, and console.rb routes them into Browser#page_errors rather
|
|
13
|
-
// than #console_logs. Keeping the two apart is deliberate: console_logs stays
|
|
14
|
-
// "the console.* calls the page made", which is what Chrome models (an
|
|
15
|
-
// exception is exceptionThrown, not consoleAPICalled) and what Playwright and
|
|
16
|
-
// Puppeteer expose (pageerror, separate from console). Folding exceptions into
|
|
17
|
-
// console_logs would also break every suite already asserting that buffer is
|
|
18
|
-
// free of errors.
|
|
19
|
-
//
|
|
20
|
-
// Delete this file once upstream emits Runtime.exceptionThrown and the floor
|
|
21
|
-
// covers it: subscribe Browser::Console to that event and feed the same buffer.
|
|
22
|
-
function _reportPageError(fields) {
|
|
23
|
-
try {
|
|
24
|
-
console.debug('__lightpanda_page_error_' + JSON.stringify(fields));
|
|
25
|
-
} catch (e) {
|
|
26
|
-
// A field that won't serialize (a getter that throws, a cyclic `reason`)
|
|
27
|
-
// must not turn one page error into two, so report the kind and move on.
|
|
28
|
-
try {
|
|
29
|
-
console.debug('__lightpanda_page_error_' + JSON.stringify({
|
|
30
|
-
kind: fields && fields.kind ? fields.kind : 'error',
|
|
31
|
-
message: '(page error could not be serialized)'
|
|
32
|
-
}));
|
|
33
|
-
} catch (e2) {}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
window.addEventListener('error', function (e) {
|
|
38
|
-
// A failed <img>/<script>/<link> fetch fires `error` too, but on the element
|
|
39
|
-
// and without bubbling, so it shouldn't reach this listener. Guard anyway
|
|
40
|
-
// (Lightpanda's dispatch need not match Chrome's exactly) and identify those
|
|
41
|
-
// by their missing string `message` — a resource failure is a network fact
|
|
42
|
-
// and Network#traffic already carries it.
|
|
43
|
-
if (!e || typeof e.message !== 'string') return;
|
|
44
|
-
|
|
45
|
-
_reportPageError({
|
|
46
|
-
kind: 'error',
|
|
47
|
-
message: e.message,
|
|
48
|
-
url: typeof e.filename === 'string' ? e.filename : '',
|
|
49
|
-
line: typeof e.lineno === 'number' ? e.lineno : null,
|
|
50
|
-
column: typeof e.colno === 'number' ? e.colno : null,
|
|
51
|
-
// Cap the stack: it rides a console argument over the CDP WebSocket, and a
|
|
52
|
-
// deep recursion stack can be megabytes.
|
|
53
|
-
stack: e.error && typeof e.error.stack === 'string' ? e.error.stack.slice(0, 4000) : null
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
window.addEventListener('unhandledrejection', function (e) {
|
|
58
|
-
var reason = e ? e.reason : null;
|
|
59
|
-
var message;
|
|
60
|
-
try {
|
|
61
|
-
message = reason && reason.message ? String(reason.message) : String(reason);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
message = '(rejection reason could not be stringified)';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
_reportPageError({
|
|
67
|
-
kind: 'unhandledrejection',
|
|
68
|
-
message: message,
|
|
69
|
-
url: '',
|
|
70
|
-
line: null,
|
|
71
|
-
column: null,
|
|
72
|
-
stack: reason && typeof reason.stack === 'string' ? reason.stack.slice(0, 4000) : null
|
|
73
|
-
});
|
|
74
|
-
});
|