playwright-ruby-client 0.7.1 → 0.8.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -0
  3. data/documentation/docs/api/element_handle.md +11 -2
  4. data/documentation/docs/api/frame.md +15 -1
  5. data/documentation/docs/api/page.md +15 -1
  6. data/documentation/docs/api/response.md +16 -0
  7. data/documentation/docs/include/api_coverage.md +6 -0
  8. data/lib/playwright.rb +36 -3
  9. data/lib/playwright/channel_owners/element_handle.rb +11 -4
  10. data/lib/playwright/channel_owners/frame.rb +14 -2
  11. data/lib/playwright/channel_owners/page.rb +13 -2
  12. data/lib/playwright/channel_owners/response.rb +8 -0
  13. data/lib/playwright/connection.rb +2 -4
  14. data/lib/playwright/transport.rb +0 -1
  15. data/lib/playwright/version.rb +2 -2
  16. data/lib/playwright/web_socket_client.rb +164 -0
  17. data/lib/playwright/web_socket_transport.rb +104 -0
  18. data/lib/playwright_api/android.rb +6 -6
  19. data/lib/playwright_api/android_device.rb +8 -8
  20. data/lib/playwright_api/browser.rb +6 -6
  21. data/lib/playwright_api/browser_context.rb +6 -6
  22. data/lib/playwright_api/browser_type.rb +6 -6
  23. data/lib/playwright_api/cdp_session.rb +12 -12
  24. data/lib/playwright_api/console_message.rb +6 -6
  25. data/lib/playwright_api/dialog.rb +6 -6
  26. data/lib/playwright_api/element_handle.rb +17 -11
  27. data/lib/playwright_api/frame.rb +20 -9
  28. data/lib/playwright_api/js_handle.rb +6 -6
  29. data/lib/playwright_api/page.rb +20 -9
  30. data/lib/playwright_api/playwright.rb +6 -6
  31. data/lib/playwright_api/request.rb +6 -6
  32. data/lib/playwright_api/response.rb +16 -6
  33. data/lib/playwright_api/route.rb +11 -6
  34. data/lib/playwright_api/selectors.rb +6 -6
  35. data/lib/playwright_api/web_socket.rb +6 -6
  36. data/lib/playwright_api/worker.rb +6 -6
  37. metadata +6 -4
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.7.1'
5
- COMPATIBLE_PLAYWRIGHT_VERSION = '1.12.0'
4
+ VERSION = '0.8.0'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.13.0'
6
6
  end
@@ -0,0 +1,164 @@
1
+ require 'openssl'
2
+ require 'socket'
3
+
4
+ begin
5
+ require 'websocket/driver'
6
+ rescue LoadError
7
+ raise "websocket-driver is required. Add `gem 'websocket-driver'` to your Gemfile"
8
+ end
9
+
10
+ # ref: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/client_socket.rb
11
+ # ref: https://github.com/cavalle/chrome_remote/blob/master/lib/chrome_remote/web_socket_client.rb
12
+ module Playwright
13
+ class WebSocketClient
14
+ class SecureSocketFactory
15
+ def initialize(host, port)
16
+ @host = host
17
+ @port = port || 443
18
+ end
19
+
20
+ def create
21
+ tcp_socket = TCPSocket.new(@host, @port)
22
+ OpenSSL::SSL::SSLSocket.new(tcp_socket).tap(&:connect)
23
+ end
24
+ end
25
+
26
+ class DriverImpl # providing #url, #write(string)
27
+ def initialize(url)
28
+ @url = url
29
+
30
+ endpoint = URI.parse(url)
31
+ @socket =
32
+ if endpoint.scheme == 'wss'
33
+ SecureSocketFactory.new(endpoint.host, endpoint.port).create
34
+ else
35
+ TCPSocket.new(endpoint.host, endpoint.port)
36
+ end
37
+ end
38
+
39
+ attr_reader :url
40
+
41
+ def write(data)
42
+ @socket.write(data)
43
+ rescue Errno::EPIPE
44
+ raise EOFError.new('already closed')
45
+ rescue Errno::ECONNRESET
46
+ raise EOFError.new('closed by remote')
47
+ end
48
+
49
+ def readpartial(maxlen = 1024)
50
+ @socket.readpartial(maxlen)
51
+ rescue Errno::ECONNRESET
52
+ raise EOFError.new('closed by remote')
53
+ end
54
+
55
+ def disconnect
56
+ @socket.close
57
+ end
58
+ end
59
+
60
+ STATE_CONNECTING = 0
61
+ STATE_OPENED = 1
62
+ STATE_CLOSING = 2
63
+ STATE_CLOSED = 3
64
+
65
+ def initialize(url:, max_payload_size:)
66
+ @impl = DriverImpl.new(url)
67
+ @driver = ::WebSocket::Driver.client(@impl, max_length: max_payload_size)
68
+
69
+ setup
70
+ end
71
+
72
+ class TransportError < StandardError; end
73
+
74
+ private def setup
75
+ @ready_state = STATE_CONNECTING
76
+ @driver.on(:open) do
77
+ @ready_state = STATE_OPENED
78
+ handle_on_open
79
+ end
80
+ @driver.on(:close) do |event|
81
+ @ready_state = STATE_CLOSED
82
+ handle_on_close(reason: event.reason, code: event.code)
83
+ end
84
+ @driver.on(:error) do |event|
85
+ if !handle_on_error(error_message: event.message)
86
+ raise TransportError.new(event.message)
87
+ end
88
+ end
89
+ @driver.on(:message) do |event|
90
+ handle_on_message(event.data)
91
+ end
92
+ end
93
+
94
+ private def wait_for_data
95
+ @driver.parse(@impl.readpartial)
96
+ end
97
+
98
+ def start
99
+ @driver.start
100
+
101
+ Thread.new do
102
+ wait_for_data until @ready_state >= STATE_CLOSING
103
+ rescue EOFError
104
+ # Google Chrome was gone.
105
+ # We have nothing todo. Just finish polling.
106
+ if @ready_state < STATE_CLOSING
107
+ handle_on_close(reason: 'Going Away', code: 1001)
108
+ end
109
+ end
110
+ end
111
+
112
+ # @param message [String]
113
+ def send_text(message)
114
+ return if @ready_state >= STATE_CLOSING
115
+ @driver.text(message)
116
+ end
117
+
118
+ def close(code: 1000, reason: "")
119
+ return if @ready_state >= STATE_CLOSING
120
+ @ready_state = STATE_CLOSING
121
+ @driver.close(reason, code)
122
+ end
123
+
124
+ def on_open(&block)
125
+ @on_open = block
126
+ end
127
+
128
+ # @param block [Proc(reason: String, code: Numeric)]
129
+ def on_close(&block)
130
+ @on_close = block
131
+ end
132
+
133
+ # @param block [Proc(error_message: String)]
134
+ def on_error(&block)
135
+ @on_error = block
136
+ end
137
+
138
+ def on_message(&block)
139
+ @on_message = block
140
+ end
141
+
142
+ private def handle_on_open
143
+ @on_open&.call
144
+ end
145
+
146
+ private def handle_on_close(reason:, code:)
147
+ @on_close&.call(reason, code)
148
+ @impl.disconnect
149
+ end
150
+
151
+ private def handle_on_error(error_message:)
152
+ return false if @on_error.nil?
153
+
154
+ @on_error.call(error_message)
155
+ true
156
+ end
157
+
158
+ private def handle_on_message(data)
159
+ return if @ready_state != STATE_OPENED
160
+
161
+ @on_message&.call(data)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Playwright
6
+ # ref: https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_transport.py
7
+ class WebSocketTransport
8
+ # @param ws_endpoint [String] EndpointURL of WebSocket
9
+ def initialize(ws_endpoint:)
10
+ @ws_endpoint = ws_endpoint
11
+ @debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
12
+ end
13
+
14
+ def on_message_received(&block)
15
+ @on_message = block
16
+ end
17
+
18
+ def on_driver_crashed(&block)
19
+ @on_driver_crashed = block
20
+ end
21
+
22
+ class AlreadyDisconnectedError < StandardError ; end
23
+
24
+ # @param message [Hash]
25
+ def send_message(message)
26
+ debug_send_message(message) if @debug
27
+ msg = JSON.dump(message)
28
+
29
+ @ws.send_text(msg)
30
+ rescue Errno::EPIPE, IOError
31
+ raise AlreadyDisconnectedError.new('send_message failed')
32
+ end
33
+
34
+ # Terminate playwright-cli driver.
35
+ def stop
36
+ return unless @ws
37
+
38
+ future = Concurrent::Promises.resolvable_future
39
+
40
+ @ws.on_close do
41
+ future.fulfill(nil)
42
+ end
43
+
44
+ begin
45
+ @ws.close
46
+ rescue EOFError => err
47
+ # ignore EOLError. The connection is already closed.
48
+ future.fulfill(err)
49
+ end
50
+
51
+ # Wait for closed actually.
52
+ future.value!
53
+ end
54
+
55
+ # Start `playwright-cli run-driver`
56
+ #
57
+ # @note This method blocks until playwright-cli exited. Consider using Thread or Future.
58
+ def async_run
59
+ ws = WebSocketClient.new(
60
+ url: @ws_endpoint,
61
+ max_payload_size: 256 * 1024 * 1024, # 256MB
62
+ )
63
+ promise = Concurrent::Promises.resolvable_future
64
+ ws.on_open do
65
+ promise.fulfill(ws)
66
+ end
67
+ ws.on_error do |error_message|
68
+ promise.reject(WebSocketClient::TransportError.new(error_message))
69
+ end
70
+
71
+ # Some messages can be sent just after start, before setting @ws.on_message
72
+ # So set this handler before ws.start.
73
+ ws.on_message do |data|
74
+ handle_on_message(data)
75
+ end
76
+
77
+ ws.start
78
+ @ws = promise.value!
79
+ @ws.on_error do |error|
80
+ puts "[WebSocketTransport] error: #{error}"
81
+ @on_driver_crashed&.call
82
+ end
83
+ rescue Errno::ECONNREFUSED => err
84
+ raise WebSocketClient::TransportError.new(err)
85
+ end
86
+
87
+ private
88
+
89
+ def handle_on_message(data)
90
+ obj = JSON.parse(data)
91
+
92
+ debug_recv_message(obj) if @debug
93
+ @on_message&.call(obj)
94
+ end
95
+
96
+ def debug_send_message(message)
97
+ puts "\x1b[33mSEND>\x1b[0m#{message}"
98
+ end
99
+
100
+ def debug_recv_message(message)
101
+ puts "\x1b[33mRECV>\x1b[0m#{message}"
102
+ end
103
+ end
104
+ end
@@ -25,20 +25,20 @@ module Playwright
25
25
 
26
26
  # -- inherited from EventEmitter --
27
27
  # @nodoc
28
- def once(event, callback)
29
- event_emitter_proxy.once(event, callback)
28
+ def off(event, callback)
29
+ event_emitter_proxy.off(event, callback)
30
30
  end
31
31
 
32
32
  # -- inherited from EventEmitter --
33
33
  # @nodoc
34
- def on(event, callback)
35
- event_emitter_proxy.on(event, callback)
34
+ def once(event, callback)
35
+ event_emitter_proxy.once(event, callback)
36
36
  end
37
37
 
38
38
  # -- inherited from EventEmitter --
39
39
  # @nodoc
40
- def off(event, callback)
41
- event_emitter_proxy.off(event, callback)
40
+ def on(event, callback)
41
+ event_emitter_proxy.on(event, callback)
42
42
  end
43
43
 
44
44
  private def event_emitter_proxy
@@ -161,14 +161,20 @@ module Playwright
161
161
  raise NotImplementedError.new('web_views is not implemented yet.')
162
162
  end
163
163
 
164
+ # @nodoc
165
+ def tap_on(selector, duration: nil, timeout: nil)
166
+ wrap_impl(@impl.tap_on(unwrap_impl(selector), duration: unwrap_impl(duration), timeout: unwrap_impl(timeout)))
167
+ end
168
+
164
169
  # @nodoc
165
170
  def tree
166
171
  wrap_impl(@impl.tree)
167
172
  end
168
173
 
174
+ # -- inherited from EventEmitter --
169
175
  # @nodoc
170
- def tap_on(selector, duration: nil, timeout: nil)
171
- wrap_impl(@impl.tap_on(unwrap_impl(selector), duration: unwrap_impl(duration), timeout: unwrap_impl(timeout)))
176
+ def off(event, callback)
177
+ event_emitter_proxy.off(event, callback)
172
178
  end
173
179
 
174
180
  # -- inherited from EventEmitter --
@@ -183,12 +189,6 @@ module Playwright
183
189
  event_emitter_proxy.on(event, callback)
184
190
  end
185
191
 
186
- # -- inherited from EventEmitter --
187
- # @nodoc
188
- def off(event, callback)
189
- event_emitter_proxy.off(event, callback)
190
- end
191
-
192
192
  private def event_emitter_proxy
193
193
  @event_emitter_proxy ||= EventEmitterProxy.new(self, @impl)
194
194
  end
@@ -158,20 +158,20 @@ module Playwright
158
158
 
159
159
  # -- inherited from EventEmitter --
160
160
  # @nodoc
161
- def once(event, callback)
162
- event_emitter_proxy.once(event, callback)
161
+ def off(event, callback)
162
+ event_emitter_proxy.off(event, callback)
163
163
  end
164
164
 
165
165
  # -- inherited from EventEmitter --
166
166
  # @nodoc
167
- def on(event, callback)
168
- event_emitter_proxy.on(event, callback)
167
+ def once(event, callback)
168
+ event_emitter_proxy.once(event, callback)
169
169
  end
170
170
 
171
171
  # -- inherited from EventEmitter --
172
172
  # @nodoc
173
- def off(event, callback)
174
- event_emitter_proxy.off(event, callback)
173
+ def on(event, callback)
174
+ event_emitter_proxy.on(event, callback)
175
175
  end
176
176
 
177
177
  private def event_emitter_proxy
@@ -384,20 +384,20 @@ module Playwright
384
384
 
385
385
  # -- inherited from EventEmitter --
386
386
  # @nodoc
387
- def once(event, callback)
388
- event_emitter_proxy.once(event, callback)
387
+ def off(event, callback)
388
+ event_emitter_proxy.off(event, callback)
389
389
  end
390
390
 
391
391
  # -- inherited from EventEmitter --
392
392
  # @nodoc
393
- def on(event, callback)
394
- event_emitter_proxy.on(event, callback)
393
+ def once(event, callback)
394
+ event_emitter_proxy.once(event, callback)
395
395
  end
396
396
 
397
397
  # -- inherited from EventEmitter --
398
398
  # @nodoc
399
- def off(event, callback)
400
- event_emitter_proxy.off(event, callback)
399
+ def on(event, callback)
400
+ event_emitter_proxy.on(event, callback)
401
401
  end
402
402
 
403
403
  private def event_emitter_proxy
@@ -145,20 +145,20 @@ module Playwright
145
145
 
146
146
  # -- inherited from EventEmitter --
147
147
  # @nodoc
148
- def once(event, callback)
149
- event_emitter_proxy.once(event, callback)
148
+ def off(event, callback)
149
+ event_emitter_proxy.off(event, callback)
150
150
  end
151
151
 
152
152
  # -- inherited from EventEmitter --
153
153
  # @nodoc
154
- def on(event, callback)
155
- event_emitter_proxy.on(event, callback)
154
+ def once(event, callback)
155
+ event_emitter_proxy.once(event, callback)
156
156
  end
157
157
 
158
158
  # -- inherited from EventEmitter --
159
159
  # @nodoc
160
- def off(event, callback)
161
- event_emitter_proxy.off(event, callback)
160
+ def on(event, callback)
161
+ event_emitter_proxy.on(event, callback)
162
162
  end
163
163
 
164
164
  private def event_emitter_proxy
@@ -13,12 +13,12 @@ module Playwright
13
13
  #
14
14
  # ```python sync
15
15
  # client = page.context().new_cdp_session(page)
16
- # client.send("animation.enable")
17
- # client.on("animation.animation_created", lambda: print("animation created!"))
18
- # response = client.send("animation.get_playback_rate")
19
- # print("playback rate is " + response["playback_rate"])
20
- # client.send("animation.set_playback_rate", {
21
- # playback_rate: response["playback_rate"] / 2
16
+ # client.send("Animation.enable")
17
+ # client.on("Animation.animationCreated", lambda: print("animation created!"))
18
+ # response = client.send("Animation.getPlaybackRate")
19
+ # print("playback rate is " + str(response["playbackRate"]))
20
+ # client.send("Animation.setPlaybackRate", {
21
+ # playbackRate: response["playbackRate"] / 2
22
22
  # })
23
23
  # ```
24
24
  class CDPSession < PlaywrightApi
@@ -35,20 +35,20 @@ module Playwright
35
35
 
36
36
  # -- inherited from EventEmitter --
37
37
  # @nodoc
38
- def once(event, callback)
39
- event_emitter_proxy.once(event, callback)
38
+ def off(event, callback)
39
+ event_emitter_proxy.off(event, callback)
40
40
  end
41
41
 
42
42
  # -- inherited from EventEmitter --
43
43
  # @nodoc
44
- def on(event, callback)
45
- event_emitter_proxy.on(event, callback)
44
+ def once(event, callback)
45
+ event_emitter_proxy.once(event, callback)
46
46
  end
47
47
 
48
48
  # -- inherited from EventEmitter --
49
49
  # @nodoc
50
- def off(event, callback)
51
- event_emitter_proxy.off(event, callback)
50
+ def on(event, callback)
51
+ event_emitter_proxy.on(event, callback)
52
52
  end
53
53
 
54
54
  private def event_emitter_proxy