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: d38fc6a0a5cbba01bba3a0a1363d8e0577dc99c2cb8a77b81f407b273a91863b
4
- data.tar.gz: 21cdea0aeba27736c52f800c22e850d80ea15d3c7bb1c6c9b173421bb227be7d
3
+ metadata.gz: 197930c8808e3cb4d58ce97952b13e657dfbf8e7d200a98b03a5e71e7afdc87f
4
+ data.tar.gz: 6401c206e53dce565b29eab2e4c4b01bbf141a88c81025bc12b584b7cc0e75af
5
5
  SHA512:
6
- metadata.gz: ec4a2743650df3602c43fc393a82af00c19c5b44d59fab31156aed0b3252e8fb7f3c5f06df1b6e2fe994c9b974a1b21b29707c22c6bff571d4c2b6aca195add4
7
- data.tar.gz: ba19fbd3c2754eae2b6d3af02584d739eba498b93e0e0ff392f9dc7a9feefdc1af1ff68081cd5b6963ca41c768cf8cebcf3b48b9debd9923e5142b15ac1c9223
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
- def send_message(message)
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 { @driver.text(message) }
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
- @socket.write(data)
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
- var notCancelled = hit.dispatchEvent(clickEvt);
642
- if (!notCancelled || clickEvt.defaultPrevented) return;
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Lightpanda
5
- VERSION = "0.14.1"
5
+ VERSION = "0.14.2"
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.14.1
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Navid Emad