playwright-ruby-client 0.0.3
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 +7 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +3 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/playwright.rb +39 -0
- data/lib/playwright/channel.rb +28 -0
- data/lib/playwright/channel_owner.rb +80 -0
- data/lib/playwright/channel_owners/android.rb +3 -0
- data/lib/playwright/channel_owners/binding_call.rb +4 -0
- data/lib/playwright/channel_owners/browser.rb +80 -0
- data/lib/playwright/channel_owners/browser_context.rb +13 -0
- data/lib/playwright/channel_owners/browser_type.rb +26 -0
- data/lib/playwright/channel_owners/chromium_browser.rb +8 -0
- data/lib/playwright/channel_owners/chromium_browser_context.rb +8 -0
- data/lib/playwright/channel_owners/electron.rb +3 -0
- data/lib/playwright/channel_owners/firefox_browser.rb +8 -0
- data/lib/playwright/channel_owners/frame.rb +44 -0
- data/lib/playwright/channel_owners/page.rb +53 -0
- data/lib/playwright/channel_owners/playwright.rb +57 -0
- data/lib/playwright/channel_owners/request.rb +5 -0
- data/lib/playwright/channel_owners/response.rb +5 -0
- data/lib/playwright/channel_owners/selectors.rb +4 -0
- data/lib/playwright/channel_owners/webkit_browser.rb +8 -0
- data/lib/playwright/connection.rb +238 -0
- data/lib/playwright/errors.rb +35 -0
- data/lib/playwright/event_emitter.rb +62 -0
- data/lib/playwright/events.rb +86 -0
- data/lib/playwright/playwright_api.rb +75 -0
- data/lib/playwright/transport.rb +86 -0
- data/lib/playwright/version.rb +5 -0
- data/lib/playwright_api/accessibility.rb +39 -0
- data/lib/playwright_api/binding_call.rb +5 -0
- data/lib/playwright_api/browser.rb +123 -0
- data/lib/playwright_api/browser_context.rb +285 -0
- data/lib/playwright_api/browser_type.rb +144 -0
- data/lib/playwright_api/cdp_session.rb +34 -0
- data/lib/playwright_api/chromium_browser_context.rb +26 -0
- data/lib/playwright_api/console_message.rb +22 -0
- data/lib/playwright_api/dialog.rb +46 -0
- data/lib/playwright_api/download.rb +54 -0
- data/lib/playwright_api/element_handle.rb +361 -0
- data/lib/playwright_api/file_chooser.rb +31 -0
- data/lib/playwright_api/frame.rb +526 -0
- data/lib/playwright_api/js_handle.rb +69 -0
- data/lib/playwright_api/keyboard.rb +101 -0
- data/lib/playwright_api/mouse.rb +47 -0
- data/lib/playwright_api/page.rb +986 -0
- data/lib/playwright_api/playwright.rb +35 -0
- data/lib/playwright_api/request.rb +119 -0
- data/lib/playwright_api/response.rb +61 -0
- data/lib/playwright_api/route.rb +53 -0
- data/lib/playwright_api/selectors.rb +51 -0
- data/lib/playwright_api/touchscreen.rb +10 -0
- data/lib/playwright_api/video.rb +14 -0
- data/lib/playwright_api/web_socket.rb +21 -0
- data/lib/playwright_api/worker.rb +34 -0
- data/playwright.gemspec +35 -0
- metadata +216 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module Playwright
|
2
|
+
define_channel_owner :BrowserType do
|
3
|
+
def name
|
4
|
+
@initializer['name']
|
5
|
+
end
|
6
|
+
|
7
|
+
def executable_path
|
8
|
+
@initializer['executablePath']
|
9
|
+
end
|
10
|
+
|
11
|
+
def launch(options, &block)
|
12
|
+
resp = @channel.send_message_to_server('launch', options.compact)
|
13
|
+
browser = ChannelOwners::Browser.from(resp)
|
14
|
+
|
15
|
+
if block
|
16
|
+
begin
|
17
|
+
block.call(browser)
|
18
|
+
ensure
|
19
|
+
browser.close
|
20
|
+
end
|
21
|
+
else
|
22
|
+
browser
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Playwright
|
2
|
+
# @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_frame.py
|
3
|
+
define_channel_owner :Frame do
|
4
|
+
def after_initialize
|
5
|
+
@event_emitter = Object.new.extend(EventEmitter)
|
6
|
+
if @initializer['parentFrame']
|
7
|
+
@parent_frame = self.from(@initializer['parentFrame'])
|
8
|
+
@parent_frame.send(:append_child_frame_from_child, self)
|
9
|
+
end
|
10
|
+
@name = @initializer['name']
|
11
|
+
@url = @initializer['url']
|
12
|
+
@detached = false
|
13
|
+
@child_frames = Set.new
|
14
|
+
@load_states = Set.new(@initializer['loadStates'])
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :page
|
18
|
+
|
19
|
+
def goto(url, timeout: nil, waitUntil: nil, referer: nil)
|
20
|
+
params = {
|
21
|
+
url: url,
|
22
|
+
timeout: timeout,
|
23
|
+
waitUntil: waitUntil,
|
24
|
+
referer: referer
|
25
|
+
}.compact
|
26
|
+
resp = @channel.send_message_to_server('goto', params)
|
27
|
+
ChannelOwners::Response.from(resp)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# @param page [Page]
|
33
|
+
# @note This method should be used internally. Accessed via .send method, so keep private!
|
34
|
+
def update_page_from_page(page)
|
35
|
+
@page = page
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param child [Frame]
|
39
|
+
# @note This method should be used internally. Accessed via .send method, so keep private!
|
40
|
+
def append_child_frame_from_child(frame)
|
41
|
+
@child_frames << frame
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Playwright
|
4
|
+
# @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_page.py
|
5
|
+
define_channel_owner :Page do
|
6
|
+
attr_writer :owned_context
|
7
|
+
|
8
|
+
def after_initialize
|
9
|
+
@accessibility = Accessibility.new(@channel)
|
10
|
+
@keyboard = Keyboard.new(@channel)
|
11
|
+
@mouse = Mouse.new(@channel)
|
12
|
+
@touchscreen = Touchscreen.new(@channel)
|
13
|
+
|
14
|
+
@main_frame = ChannelOwners::Frame.from(@initializer['mainFrame'])
|
15
|
+
@main_frame.send(:update_page_from_page, self)
|
16
|
+
@frames = Set.new
|
17
|
+
@frames << @main_frame
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :accessibility, :keyboard, :mouse, :touchscreen, :main_frame
|
21
|
+
|
22
|
+
def goto(url, timeout: nil, waitUntil: nil, referer: nil)
|
23
|
+
@main_frame.goto(url, timeout: timeout, waitUntil: waitUntil, referer: referer)
|
24
|
+
end
|
25
|
+
|
26
|
+
def screenshot(
|
27
|
+
path: nil,
|
28
|
+
type: nil,
|
29
|
+
quality: nil,
|
30
|
+
fullPage: nil,
|
31
|
+
clip: nil,
|
32
|
+
omitBackground: nil,
|
33
|
+
timeout: nil)
|
34
|
+
|
35
|
+
params = {
|
36
|
+
type: type,
|
37
|
+
quality: quality,
|
38
|
+
fullPage: fullPage,
|
39
|
+
clip: clip,
|
40
|
+
omitBackground: omitBackground,
|
41
|
+
timeout: timeout,
|
42
|
+
}.compact
|
43
|
+
encoded_binary = @channel.send_message_to_server('screenshot', params)
|
44
|
+
decoded_binary = Base64.decode64(encoded_binary)
|
45
|
+
if path
|
46
|
+
File.open(path, 'wb') do |f|
|
47
|
+
f.write(decoded_binary)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
decoded_binary
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Playwright
|
2
|
+
define_channel_owner :Playwright do
|
3
|
+
def chromium
|
4
|
+
@chromium ||= ::Playwright::ChannelOwners::BrowserType.from(@initializer['chromium'])
|
5
|
+
end
|
6
|
+
|
7
|
+
def firefox
|
8
|
+
@firefox ||= ::Playwright::ChannelOwners::BrowserType.from(@initializer['firefox'])
|
9
|
+
end
|
10
|
+
|
11
|
+
def webkit
|
12
|
+
@webkit ||= ::Playwright::ChannelOwners::BrowserType.from(@initializer['webkit'])
|
13
|
+
end
|
14
|
+
|
15
|
+
def android
|
16
|
+
@android ||= ::Playwright::ChannelOwners::Android.from(@initializer['android'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def electron
|
20
|
+
@electron ||= ::Playwright::ChannelOwners::Electron.from(@initializer['electron'])
|
21
|
+
end
|
22
|
+
|
23
|
+
class DeviceDescriptor
|
24
|
+
class Viewport
|
25
|
+
def initialize(hash)
|
26
|
+
@width = hash['width']
|
27
|
+
@heirhgt = hash['height']
|
28
|
+
end
|
29
|
+
attr_reader :width, :height
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(hash)
|
33
|
+
@user_agent = hash["userAgent"]
|
34
|
+
@viewport = Viewport.new(hash["viewport"])
|
35
|
+
@device_scale_factor = hash["deviceScaleFactor"]
|
36
|
+
@is_mobile = hash["isMobile"]
|
37
|
+
@has_touch = hash["hasTouch"]
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :user_agent, :viewport, :device_scale_factor
|
41
|
+
|
42
|
+
def mobile?
|
43
|
+
@is_mobile
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_touch?
|
47
|
+
@has_touch
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def devices
|
52
|
+
@devices ||= @initializer['deviceDescriptors'].map do |item|
|
53
|
+
[item['name'], DeviceDescriptor.new(item['descriptor'])]
|
54
|
+
end.to_h
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Playwright
|
4
|
+
# https://github.com/microsoft/playwright/blob/master/src/client/connection.ts
|
5
|
+
# https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_connection.py
|
6
|
+
# https://github.com/microsoft/playwright-java/blob/master/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
|
7
|
+
class Connection
|
8
|
+
def initialize(playwright_cli_executable_path:)
|
9
|
+
@transport = Transport.new(
|
10
|
+
playwright_cli_executable_path: playwright_cli_executable_path
|
11
|
+
)
|
12
|
+
@transport.on_message_received do |message|
|
13
|
+
dispatch(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
@objects = {} # Hash[ guid => ChannelOwner ]
|
17
|
+
@waiting_for_object = {} # Hash[ guid => Promise<ChannelOwner> ]
|
18
|
+
@callbacks = {} # Hash [ guid => Promise<ChannelOwner> ]
|
19
|
+
@root_object = RootChannelOwner.new(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
@transport.run
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@transport.stop
|
28
|
+
end
|
29
|
+
|
30
|
+
def async_wait_for_object_with_known_name(guid)
|
31
|
+
if @objects[guid]
|
32
|
+
return @objects[guid]
|
33
|
+
end
|
34
|
+
|
35
|
+
callback = Concurrent::Promises.resolvable_future
|
36
|
+
@waiting_for_object[guid] = callback
|
37
|
+
callback
|
38
|
+
end
|
39
|
+
|
40
|
+
def wait_for_object_with_known_name(guid)
|
41
|
+
async_wait_for_object_with_known_name.value!
|
42
|
+
end
|
43
|
+
|
44
|
+
def async_send_message_to_server(guid, method, params)
|
45
|
+
callback = Concurrent::Promises.resolvable_future
|
46
|
+
|
47
|
+
with_generated_id do |id|
|
48
|
+
# register callback promise object first.
|
49
|
+
# @see https://github.com/YusukeIwaki/puppeteer-ruby/pull/34
|
50
|
+
@callbacks[id] = callback
|
51
|
+
|
52
|
+
message = {
|
53
|
+
id: id,
|
54
|
+
guid: guid,
|
55
|
+
method: method,
|
56
|
+
params: replace_channels_with_guids(params),
|
57
|
+
}
|
58
|
+
@transport.send_message(message)
|
59
|
+
rescue Transport::AlreadyDisconnectedError => err
|
60
|
+
@callbacks.delete(id)
|
61
|
+
callback.reject(err)
|
62
|
+
end
|
63
|
+
|
64
|
+
callback
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_message_to_server(guid, method, params)
|
68
|
+
async_send_message_to_server(guid, method, params).value!
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# ```usage
|
74
|
+
# connection.with_generated_id do |id|
|
75
|
+
# # play with id
|
76
|
+
# end
|
77
|
+
# ````
|
78
|
+
def with_generated_id(&block)
|
79
|
+
@last_id ||= 0
|
80
|
+
block.call(@last_id += 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param guid [String]
|
84
|
+
# @param parent [Playwright::ChannelOwner]
|
85
|
+
# @note This method should be used internally. Accessed via .send method from Playwright::ChannelOwner, so keep private!
|
86
|
+
def update_object_from_channel_owner(guid, parent)
|
87
|
+
@objects[guid] = parent
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param guid [String]
|
91
|
+
# @note This method should be used internally. Accessed via .send method from Playwright::ChannelOwner, so keep private!
|
92
|
+
def delete_object_from_channel_owner(guid)
|
93
|
+
@objects.delete(guid)
|
94
|
+
end
|
95
|
+
|
96
|
+
def dispatch(msg)
|
97
|
+
id = msg['id']
|
98
|
+
if id
|
99
|
+
callback = @callbacks.delete(id)
|
100
|
+
|
101
|
+
unless callback
|
102
|
+
raise "Cannot find command to respond: #{id}"
|
103
|
+
end
|
104
|
+
|
105
|
+
error = msg['error']
|
106
|
+
if error
|
107
|
+
callback.reject(::Playwright::Error.parse(error['error']))
|
108
|
+
else
|
109
|
+
result = replace_guids_with_channels(msg['result'])
|
110
|
+
callback.fulfill(result)
|
111
|
+
end
|
112
|
+
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
guid = msg['guid']
|
117
|
+
method = msg['method']
|
118
|
+
params = msg['params']
|
119
|
+
|
120
|
+
if method == "__create__"
|
121
|
+
create_remote_object(
|
122
|
+
parent_guid: guid,
|
123
|
+
type: params["type"],
|
124
|
+
guid: params["guid"],
|
125
|
+
initializer: params["initializer"],
|
126
|
+
)
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
if method == "__dispose__"
|
131
|
+
object = @objects[guid]
|
132
|
+
unless object
|
133
|
+
raise "Cannot find object to dispose: #{guid}"
|
134
|
+
end
|
135
|
+
object.dispose
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
object = @objects[guid]
|
140
|
+
unless object
|
141
|
+
raise "Cannot find object to emit \"#{method}\": #{guid}"
|
142
|
+
end
|
143
|
+
object.channel.emit(method, replace_guids_with_channels(params))
|
144
|
+
end
|
145
|
+
|
146
|
+
def replace_channels_with_guids(payload)
|
147
|
+
if payload.nil?
|
148
|
+
return nil
|
149
|
+
end
|
150
|
+
|
151
|
+
if payload.is_a?(Array)
|
152
|
+
return payload.map{ |pl| replace_channels_with_guids(pl) }
|
153
|
+
end
|
154
|
+
|
155
|
+
if payload.is_a?(Channel)
|
156
|
+
{ guid: payload.guid }
|
157
|
+
end
|
158
|
+
|
159
|
+
if payload.is_a?(Hash)
|
160
|
+
return payload.map { |k, v| [k, replace_channels_with_guids(v)] }.to_h
|
161
|
+
end
|
162
|
+
|
163
|
+
payload
|
164
|
+
end
|
165
|
+
|
166
|
+
def replace_guids_with_channels(payload)
|
167
|
+
if payload.nil?
|
168
|
+
return nil
|
169
|
+
end
|
170
|
+
|
171
|
+
if payload.is_a?(Array)
|
172
|
+
return payload.map{ |pl| replace_guids_with_channels(pl) }
|
173
|
+
end
|
174
|
+
|
175
|
+
if payload.is_a?(Hash)
|
176
|
+
guid = payload['guid']
|
177
|
+
if guid && @objects[guid]
|
178
|
+
return @objects[guid].channel
|
179
|
+
end
|
180
|
+
|
181
|
+
return payload.map { |k, v| [k, replace_guids_with_channels(v)] }.to_h
|
182
|
+
end
|
183
|
+
|
184
|
+
payload
|
185
|
+
end
|
186
|
+
|
187
|
+
# @return [Playwright::ChannelOwner|nil]
|
188
|
+
def create_remote_object(parent_guid:, type:, guid:, initializer:)
|
189
|
+
parent = @objects[parent_guid]
|
190
|
+
unless parent
|
191
|
+
raise "Cannot find parent object #{parent_guid} to create #{guid}"
|
192
|
+
end
|
193
|
+
initializer = replace_guids_with_channels(initializer)
|
194
|
+
|
195
|
+
params = [
|
196
|
+
parent,
|
197
|
+
type,
|
198
|
+
guid,
|
199
|
+
initializer,
|
200
|
+
]
|
201
|
+
class_name = case type
|
202
|
+
when 'Browser'
|
203
|
+
case initializer['name']
|
204
|
+
when 'chromium'
|
205
|
+
'ChromiumBrowser'
|
206
|
+
when 'webkit'
|
207
|
+
'WebKitBrowser'
|
208
|
+
when 'firefox'
|
209
|
+
'FirefoxBrowser'
|
210
|
+
else
|
211
|
+
'Browser'
|
212
|
+
end
|
213
|
+
when 'BrowserContext'
|
214
|
+
browser_name = initializer['browserName']
|
215
|
+
if browser_name == 'chromium'
|
216
|
+
'ChromiumBrowserContext'
|
217
|
+
else
|
218
|
+
params << browser_name
|
219
|
+
'BrowserContext'
|
220
|
+
end
|
221
|
+
else
|
222
|
+
type
|
223
|
+
end
|
224
|
+
|
225
|
+
result =
|
226
|
+
begin
|
227
|
+
ChannelOwners.const_get(class_name).new(*params)
|
228
|
+
rescue NameError
|
229
|
+
raise "Missing type #{type}"
|
230
|
+
end
|
231
|
+
|
232
|
+
callback = @waiting_for_object.delete(guid)
|
233
|
+
callback&.fulfill(result)
|
234
|
+
|
235
|
+
result
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|