playwright-ruby-client 0.6.4 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -0
  3. data/documentation/docs/api/browser.md +10 -0
  4. data/documentation/docs/api/browser_context.md +10 -0
  5. data/documentation/docs/api/browser_type.md +53 -0
  6. data/documentation/docs/api/cdp_session.md +41 -1
  7. data/documentation/docs/api/element_handle.md +11 -2
  8. data/documentation/docs/api/frame.md +15 -1
  9. data/documentation/docs/api/page.md +33 -1
  10. data/documentation/docs/api/response.md +16 -0
  11. data/documentation/docs/api/route.md +20 -21
  12. data/documentation/docs/api/web_socket.md +38 -1
  13. data/documentation/docs/article/guides/launch_browser.md +2 -0
  14. data/documentation/docs/article/guides/rails_integration.md +2 -2
  15. data/documentation/docs/article/guides/semi_automation.md +67 -0
  16. data/documentation/docs/include/api_coverage.md +19 -13
  17. data/lib/playwright.rb +36 -3
  18. data/lib/playwright/channel_owners/browser.rb +5 -0
  19. data/lib/playwright/channel_owners/browser_context.rb +5 -0
  20. data/lib/playwright/channel_owners/browser_type.rb +18 -0
  21. data/lib/playwright/channel_owners/cdp_session.rb +19 -0
  22. data/lib/playwright/channel_owners/element_handle.rb +11 -4
  23. data/lib/playwright/channel_owners/frame.rb +14 -2
  24. data/lib/playwright/channel_owners/page.rb +21 -2
  25. data/lib/playwright/channel_owners/response.rb +9 -1
  26. data/lib/playwright/channel_owners/web_socket.rb +87 -0
  27. data/lib/playwright/connection.rb +2 -4
  28. data/lib/playwright/transport.rb +0 -1
  29. data/lib/playwright/version.rb +2 -2
  30. data/lib/playwright/web_socket_client.rb +164 -0
  31. data/lib/playwright/web_socket_transport.rb +104 -0
  32. data/lib/playwright_api/android.rb +6 -6
  33. data/lib/playwright_api/android_device.rb +8 -8
  34. data/lib/playwright_api/browser.rb +7 -7
  35. data/lib/playwright_api/browser_context.rb +7 -7
  36. data/lib/playwright_api/browser_type.rb +9 -8
  37. data/lib/playwright_api/cdp_session.rb +30 -8
  38. data/lib/playwright_api/console_message.rb +6 -6
  39. data/lib/playwright_api/dialog.rb +6 -6
  40. data/lib/playwright_api/element_handle.rb +17 -11
  41. data/lib/playwright_api/frame.rb +20 -9
  42. data/lib/playwright_api/js_handle.rb +6 -6
  43. data/lib/playwright_api/page.rb +28 -17
  44. data/lib/playwright_api/playwright.rb +6 -6
  45. data/lib/playwright_api/request.rb +6 -6
  46. data/lib/playwright_api/response.rb +15 -10
  47. data/lib/playwright_api/route.rb +11 -6
  48. data/lib/playwright_api/selectors.rb +6 -6
  49. data/lib/playwright_api/web_socket.rb +28 -6
  50. data/lib/playwright_api/worker.rb +6 -6
  51. data/playwright.gemspec +2 -1
  52. metadata +37 -18
@@ -4,7 +4,7 @@ require 'json'
4
4
  module Playwright
5
5
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_network.py
6
6
  define_channel_owner :Response do
7
- def after_initialize
7
+ private def after_initialize
8
8
  @request = ChannelOwners::Request.from(@initializer['request'])
9
9
  timing = @initializer['timing']
10
10
  @request.send(:update_timings,
@@ -44,6 +44,14 @@ module Playwright
44
44
  end.to_h
45
45
  end
46
46
 
47
+ def server_addr
48
+ @channel.send_message_to_server('serverAddr')
49
+ end
50
+
51
+ def security_details
52
+ @channel.send_message_to_server('securityDetails')
53
+ end
54
+
47
55
  def finished
48
56
  @channel.send_message_to_server('finished')
49
57
  end
@@ -0,0 +1,87 @@
1
+ require 'base64'
2
+
3
+ module Playwright
4
+ define_channel_owner :WebSocket do
5
+ private def after_initialize
6
+ @closed = false
7
+
8
+ @channel.on('frameSent', -> (params) {
9
+ on_frame_sent(params['opcode'], params['data'])
10
+ })
11
+ @channel.on('frameReceived', -> (params) {
12
+ on_frame_received(params['opcode'], params['data'])
13
+ })
14
+ @channel.on('socketError', -> (params) {
15
+ emit(Events::WebSocket::Error, params['error'])
16
+ })
17
+ @channel.on('close', -> (_) { on_close })
18
+ end
19
+
20
+ def url
21
+ @initializer['url']
22
+ end
23
+
24
+ class SocketClosedError < StandardError
25
+ def initialize
26
+ super('Socket closed')
27
+ end
28
+ end
29
+
30
+ class SocketError < StandardError
31
+ def initialize
32
+ super('Socket error')
33
+ end
34
+ end
35
+
36
+ class PageClosedError < StandardError
37
+ def initialize
38
+ super('Page closed')
39
+ end
40
+ end
41
+
42
+ def expect_event(event, predicate: nil, timeout: nil, &block)
43
+ wait_helper = WaitHelper.new
44
+ wait_helper.reject_on_timeout(timeout || @parent.send(:timeout_settings).timeout, "Timeout while waiting for event \"#{event}\"")
45
+
46
+ unless event == Events::WebSocket::Close
47
+ wait_helper.reject_on_event(self, Events::WebSocket::Close, SocketClosedError.new)
48
+ end
49
+
50
+ unless event == Events::WebSocket::Error
51
+ wait_helper.reject_on_event(self, Events::WebSocket::Error, SocketError.new)
52
+ end
53
+
54
+ wait_helper.reject_on_event(@parent, 'close', PageClosedError.new)
55
+ wait_helper.wait_for_event(self, event, predicate: predicate)
56
+ block&.call
57
+
58
+ wait_helper.promise.value!
59
+ end
60
+ alias_method :wait_for_event, :expect_event
61
+
62
+ private def on_frame_sent(opcode, data)
63
+ if opcode == 2
64
+ emit(Events::WebSocket::FrameSent, Base64.strict_decode64(data))
65
+ else
66
+ emit(Events::WebSocket::FrameSent, data)
67
+ end
68
+ end
69
+
70
+ private def on_frame_received(opcode, data)
71
+ if opcode == 2
72
+ emit(Events::WebSocket::FrameReceived, Base64.strict_decode64(data))
73
+ else
74
+ emit(Events::WebSocket::FrameReceived, data)
75
+ end
76
+ end
77
+
78
+ def closed?
79
+ @closed
80
+ end
81
+
82
+ private def on_close
83
+ @closed = true
84
+ emit(Events::WebSocket::Close)
85
+ end
86
+ end
87
+ end
@@ -5,10 +5,8 @@ module Playwright
5
5
  # https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_connection.py
6
6
  # https://github.com/microsoft/playwright-java/blob/master/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
7
7
  class Connection
8
- def initialize(playwright_cli_executable_path:)
9
- @transport = Transport.new(
10
- playwright_cli_executable_path: playwright_cli_executable_path
11
- )
8
+ def initialize(transport)
9
+ @transport = transport
12
10
  @transport.on_message_received do |message|
13
11
  dispatch(message)
14
12
  end
@@ -8,7 +8,6 @@ module Playwright
8
8
  # ref: https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_transport.py
9
9
  class Transport
10
10
  # @param playwright_cli_executable_path [String] path to playwright-cli.
11
- # @param debug [Boolean]
12
11
  def initialize(playwright_cli_executable_path:)
13
12
  @driver_executable_path = playwright_cli_executable_path
14
13
  @debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.6.4'
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