capybara-lightpanda 0.14.1 → 0.14.2
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 197930c8808e3cb4d58ce97952b13e657dfbf8e7d200a98b03a5e71e7afdc87f
|
|
4
|
+
data.tar.gz: 6401c206e53dce565b29eab2e4c4b01bbf141a88c81025bc12b584b7cc0e75af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0cfe0bd2b2491e8b26715110950286a74e9bd1a7816d52a76b87cc601814ed4dd4faabf5665108f82624d6720581bdc7560c82445220456cb089a13313618857
|
|
7
|
+
data.tar.gz: 84fb972984583e315cf20f0a70a114238015aa29c49786a6aec5f329556518fc63ee5bb2204e430ccdb81e014943535cc2899c1ec40fb673d3f641bf5b201063
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.2] - 2026-10-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A browser that stops reading no longer hangs the test process.** If Lightpanda got stuck (a page in an endless synchronous loop, a stopped process) while a large command was being sent, about 1 MB and up (a big `execute_script` bundle, for example), the send blocked forever: the command's timeout never started, every other command waited behind it, and so did quitting the browser, at exit included. Sending now gives up after the command's timeout and raises `DeadBrowserError`.
|
|
8
|
+
- **Clicking a `mailto:` or `tel:` link keeps the page, as in Chrome.** On Lightpanda 1.0.0, clicking `<a href="mailto:someone@example.com">` left the page and loaded `example.com`, the host in the address. On nightlies 10261 to 10535, clicking a `tel:` link left the page unusable: every later call raised `NoExecutionContextError`. The page now stays and your own click handlers still run. The same applies to any link whose scheme the browser can't load (`sms:`, `ftp:`…).
|
|
9
|
+
|
|
3
10
|
## [0.14.1] - 2026-10-12
|
|
4
11
|
|
|
5
12
|
### Fixed
|
|
@@ -26,11 +26,18 @@ module Capybara
|
|
|
26
26
|
connect
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# `timeout` bounds how long the browser may go without taking any of
|
|
30
|
+
# the frame's bytes (see #write); nil falls back to options.timeout.
|
|
31
|
+
def send_message(message, timeout: nil)
|
|
30
32
|
raise DeadBrowserError, "WebSocket is not open" unless @status == :open
|
|
31
33
|
|
|
32
34
|
@logger&.puts("\n\n▶ #{@logger.elapsed_time} #{message}")
|
|
33
|
-
@driver_mutex.synchronize
|
|
35
|
+
@driver_mutex.synchronize do
|
|
36
|
+
@write_timeout = timeout
|
|
37
|
+
@driver.text(message)
|
|
38
|
+
ensure
|
|
39
|
+
@write_timeout = nil
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
# The socket is closed even when the connection is already dead. A
|
|
@@ -53,8 +60,25 @@ module Capybara
|
|
|
53
60
|
@status == :closed || @status == :error
|
|
54
61
|
end
|
|
55
62
|
|
|
63
|
+
# Called by websocket-driver, always under @driver_mutex. A browser
|
|
64
|
+
# that stops reading (hung, stopped, or a page stuck in a sync JS loop
|
|
65
|
+
# on Lightpanda's single thread) would block a plain write forever
|
|
66
|
+
# once the frame outgrows the socket buffers, holding the lock that
|
|
67
|
+
# every other command, the reader's parse and #close need — and the
|
|
68
|
+
# command timeout only starts after the write returns. So the write
|
|
69
|
+
# gives up once the browser takes no byte for the timeout; the wait
|
|
70
|
+
# restarts on every partial write, so a slow but moving peer is fine
|
|
71
|
+
# (ferrum #644). Close and pong frames have no command timeout and
|
|
72
|
+
# fall back to options.timeout rather than waiting forever.
|
|
56
73
|
def write(data)
|
|
57
|
-
|
|
74
|
+
until data.empty?
|
|
75
|
+
written = @socket.write_nonblock(data, exception: false)
|
|
76
|
+
if written.is_a?(Integer)
|
|
77
|
+
data = data.byteslice(written..)
|
|
78
|
+
elsif !wait_for_socket(written)
|
|
79
|
+
abandon_stalled_write
|
|
80
|
+
end
|
|
81
|
+
end
|
|
58
82
|
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
59
83
|
mark_dead
|
|
60
84
|
end
|
|
@@ -71,6 +95,21 @@ module Capybara
|
|
|
71
95
|
|
|
72
96
|
private
|
|
73
97
|
|
|
98
|
+
def wait_for_socket(state)
|
|
99
|
+
timeout = @write_timeout || @options.timeout
|
|
100
|
+
state == :wait_readable ? @socket.wait_readable(timeout) : @socket.wait_writable(timeout)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The frame was cut off midway, so the connection can't be reused:
|
|
104
|
+
# mark it dead (the reader and message threads stop, pending commands
|
|
105
|
+
# are released) and close the socket.
|
|
106
|
+
def abandon_stalled_write
|
|
107
|
+
timeout = @write_timeout || @options.timeout
|
|
108
|
+
mark_dead(:error)
|
|
109
|
+
close_socket
|
|
110
|
+
raise DeadBrowserError, "Browser accepted no data for #{timeout}s and is likely stuck; connection closed"
|
|
111
|
+
end
|
|
112
|
+
|
|
74
113
|
def close_socket
|
|
75
114
|
@socket&.close unless @socket.nil? || @socket.closed?
|
|
76
115
|
rescue IOError, SystemCallError
|
|
@@ -22,18 +22,18 @@ module Capybara
|
|
|
22
22
|
|
|
23
23
|
def command(method, params = {}, async: false, session_id: nil, timeout: nil)
|
|
24
24
|
message = build_message(method, params, session_id: session_id)
|
|
25
|
+
effective_timeout = timeout || @options.timeout
|
|
25
26
|
|
|
26
27
|
if async
|
|
27
|
-
@ws.send_message(JSON.generate(message))
|
|
28
|
+
@ws.send_message(JSON.generate(message), timeout: effective_timeout)
|
|
28
29
|
return true
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
pending = Concurrent::IVar.new
|
|
32
33
|
@pendings[message[:id]] = pending
|
|
33
34
|
|
|
34
|
-
@ws.send_message(JSON.generate(message))
|
|
35
|
+
@ws.send_message(JSON.generate(message), timeout: effective_timeout)
|
|
35
36
|
|
|
36
|
-
effective_timeout = timeout || @options.timeout
|
|
37
37
|
response = pending.value!(effective_timeout)
|
|
38
38
|
|
|
39
39
|
if response.nil?
|
|
@@ -638,8 +638,27 @@ module Capybara
|
|
|
638
638
|
}
|
|
639
639
|
hit.dispatchEvent(new EventCtor('mouseup', { bubbles: true, cancelable: true }));
|
|
640
640
|
var clickEvt = new EventCtor('click', { bubbles: true, cancelable: true });
|
|
641
|
-
|
|
642
|
-
|
|
641
|
+
// A link to a scheme the browser can't fetch (mailto:, tel:, sms:…)
|
|
642
|
+
// goes to an external app in Chrome and the page stays. Below
|
|
643
|
+
// upstream #3940 (build 10536) the link's own activation instead
|
|
644
|
+
// turns `mailto:a@b.com` into `http://mailto:a@b.com/` and loads
|
|
645
|
+
// b.com (1.0.0), or loses the execution context on a failed `tel:`
|
|
646
|
+
// navigation (10261–10535). Cancel that default from a window
|
|
647
|
+
// listener added for this one dispatch: it runs after the page's
|
|
648
|
+
// own listeners, which still see an uncancelled click.
|
|
649
|
+
var link = hit.closest ? hit.closest('a[href]') : null;
|
|
650
|
+
var scheme = link && /^\\s*([a-z][a-z0-9+.-]*):/i.exec(link.getAttribute('href'));
|
|
651
|
+
var external = !!scheme && !/^(https?|about|blob|data|file|javascript)$/i.test(scheme[1]);
|
|
652
|
+
var view = hit.ownerDocument.defaultView;
|
|
653
|
+
var keepPage = function (e) { if (e === clickEvt) e.preventDefault(); };
|
|
654
|
+
if (external && view) view.addEventListener('click', keepPage);
|
|
655
|
+
var notCancelled;
|
|
656
|
+
try {
|
|
657
|
+
notCancelled = hit.dispatchEvent(clickEvt);
|
|
658
|
+
} finally {
|
|
659
|
+
if (external && view) view.removeEventListener('click', keepPage);
|
|
660
|
+
}
|
|
661
|
+
if (external || !notCancelled || clickEvt.defaultPrevented) return;
|
|
643
662
|
var tag = hit.tagName;
|
|
644
663
|
// A `javascript:` link never navigates: the click's own activation
|
|
645
664
|
// behavior has already run its code. Assigning it to location.href
|