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.
- checksums.yaml +4 -4
- data/README.md +26 -0
- data/documentation/docs/api/browser.md +10 -0
- data/documentation/docs/api/browser_context.md +10 -0
- data/documentation/docs/api/browser_type.md +53 -0
- data/documentation/docs/api/cdp_session.md +41 -1
- data/documentation/docs/api/element_handle.md +11 -2
- data/documentation/docs/api/frame.md +15 -1
- data/documentation/docs/api/page.md +33 -1
- data/documentation/docs/api/response.md +16 -0
- data/documentation/docs/api/route.md +20 -21
- data/documentation/docs/api/web_socket.md +38 -1
- data/documentation/docs/article/guides/launch_browser.md +2 -0
- data/documentation/docs/article/guides/rails_integration.md +2 -2
- data/documentation/docs/article/guides/semi_automation.md +67 -0
- data/documentation/docs/include/api_coverage.md +19 -13
- data/lib/playwright.rb +36 -3
- data/lib/playwright/channel_owners/browser.rb +5 -0
- data/lib/playwright/channel_owners/browser_context.rb +5 -0
- data/lib/playwright/channel_owners/browser_type.rb +18 -0
- data/lib/playwright/channel_owners/cdp_session.rb +19 -0
- data/lib/playwright/channel_owners/element_handle.rb +11 -4
- data/lib/playwright/channel_owners/frame.rb +14 -2
- data/lib/playwright/channel_owners/page.rb +21 -2
- data/lib/playwright/channel_owners/response.rb +9 -1
- data/lib/playwright/channel_owners/web_socket.rb +87 -0
- data/lib/playwright/connection.rb +2 -4
- data/lib/playwright/transport.rb +0 -1
- data/lib/playwright/version.rb +2 -2
- data/lib/playwright/web_socket_client.rb +164 -0
- data/lib/playwright/web_socket_transport.rb +104 -0
- data/lib/playwright_api/android.rb +6 -6
- data/lib/playwright_api/android_device.rb +8 -8
- data/lib/playwright_api/browser.rb +7 -7
- data/lib/playwright_api/browser_context.rb +7 -7
- data/lib/playwright_api/browser_type.rb +9 -8
- data/lib/playwright_api/cdp_session.rb +30 -8
- data/lib/playwright_api/console_message.rb +6 -6
- data/lib/playwright_api/dialog.rb +6 -6
- data/lib/playwright_api/element_handle.rb +17 -11
- data/lib/playwright_api/frame.rb +20 -9
- data/lib/playwright_api/js_handle.rb +6 -6
- data/lib/playwright_api/page.rb +28 -17
- data/lib/playwright_api/playwright.rb +6 -6
- data/lib/playwright_api/request.rb +6 -6
- data/lib/playwright_api/response.rb +15 -10
- data/lib/playwright_api/route.rb +11 -6
- data/lib/playwright_api/selectors.rb +6 -6
- data/lib/playwright_api/web_socket.rb +28 -6
- data/lib/playwright_api/worker.rb +6 -6
- data/playwright.gemspec +2 -1
- 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(
|
9
|
-
@transport =
|
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
|
data/lib/playwright/transport.rb
CHANGED
@@ -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'
|
data/lib/playwright/version.rb
CHANGED
@@ -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
|
29
|
-
event_emitter_proxy.
|
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
|
35
|
-
event_emitter_proxy.
|
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
|
41
|
-
event_emitter_proxy.
|
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
|
171
|
-
|
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
|