playwright-ruby-client 0.0.3 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +119 -12
  3. data/docs/api_coverage.md +354 -0
  4. data/lib/playwright.rb +8 -0
  5. data/lib/playwright/channel_owner.rb +16 -2
  6. data/lib/playwright/channel_owners/android.rb +10 -1
  7. data/lib/playwright/channel_owners/android_device.rb +163 -0
  8. data/lib/playwright/channel_owners/browser.rb +22 -29
  9. data/lib/playwright/channel_owners/browser_context.rb +43 -0
  10. data/lib/playwright/channel_owners/console_message.rb +21 -0
  11. data/lib/playwright/channel_owners/element_handle.rb +314 -0
  12. data/lib/playwright/channel_owners/frame.rb +466 -7
  13. data/lib/playwright/channel_owners/js_handle.rb +55 -0
  14. data/lib/playwright/channel_owners/page.rb +353 -5
  15. data/lib/playwright/channel_owners/request.rb +90 -0
  16. data/lib/playwright/channel_owners/webkit_browser.rb +1 -1
  17. data/lib/playwright/connection.rb +15 -14
  18. data/lib/playwright/errors.rb +1 -1
  19. data/lib/playwright/event_emitter.rb +13 -0
  20. data/lib/playwright/input_files.rb +42 -0
  21. data/lib/playwright/input_type.rb +19 -0
  22. data/lib/playwright/input_types/android_input.rb +19 -0
  23. data/lib/playwright/input_types/keyboard.rb +32 -0
  24. data/lib/playwright/input_types/mouse.rb +4 -0
  25. data/lib/playwright/input_types/touchscreen.rb +4 -0
  26. data/lib/playwright/javascript.rb +13 -0
  27. data/lib/playwright/javascript/expression.rb +67 -0
  28. data/lib/playwright/javascript/function.rb +67 -0
  29. data/lib/playwright/javascript/value_parser.rb +75 -0
  30. data/lib/playwright/javascript/value_serializer.rb +54 -0
  31. data/lib/playwright/playwright_api.rb +45 -25
  32. data/lib/playwright/select_option_values.rb +32 -0
  33. data/lib/playwright/timeout_settings.rb +19 -0
  34. data/lib/playwright/url_matcher.rb +19 -0
  35. data/lib/playwright/utils.rb +37 -0
  36. data/lib/playwright/version.rb +1 -1
  37. data/lib/playwright/wait_helper.rb +73 -0
  38. data/lib/playwright_api/accessibility.rb +60 -6
  39. data/lib/playwright_api/android.rb +33 -0
  40. data/lib/playwright_api/android_device.rb +78 -0
  41. data/lib/playwright_api/android_input.rb +25 -0
  42. data/lib/playwright_api/binding_call.rb +18 -0
  43. data/lib/playwright_api/browser.rb +136 -44
  44. data/lib/playwright_api/browser_context.rb +378 -51
  45. data/lib/playwright_api/browser_type.rb +137 -55
  46. data/lib/playwright_api/cdp_session.rb +32 -7
  47. data/lib/playwright_api/chromium_browser_context.rb +31 -0
  48. data/lib/playwright_api/console_message.rb +27 -7
  49. data/lib/playwright_api/dialog.rb +47 -3
  50. data/lib/playwright_api/download.rb +29 -5
  51. data/lib/playwright_api/element_handle.rb +429 -143
  52. data/lib/playwright_api/file_chooser.rb +13 -2
  53. data/lib/playwright_api/frame.rb +633 -179
  54. data/lib/playwright_api/js_handle.rb +97 -17
  55. data/lib/playwright_api/keyboard.rb +152 -24
  56. data/lib/playwright_api/mouse.rb +28 -3
  57. data/lib/playwright_api/page.rb +1183 -317
  58. data/lib/playwright_api/playwright.rb +174 -13
  59. data/lib/playwright_api/request.rb +115 -30
  60. data/lib/playwright_api/response.rb +22 -3
  61. data/lib/playwright_api/route.rb +63 -4
  62. data/lib/playwright_api/selectors.rb +29 -7
  63. data/lib/playwright_api/touchscreen.rb +2 -1
  64. data/lib/playwright_api/video.rb +11 -1
  65. data/lib/playwright_api/web_socket.rb +5 -5
  66. data/lib/playwright_api/worker.rb +29 -5
  67. data/playwright.gemspec +3 -0
  68. metadata +68 -2
@@ -0,0 +1,55 @@
1
+ module Playwright
2
+ define_channel_owner :JSHandle do
3
+ def after_initialize
4
+ @preview = @initializer['preview']
5
+ @channel.on('previewUpdated', method(:on_preview_updated))
6
+ end
7
+
8
+ def to_s
9
+ @preview
10
+ end
11
+
12
+ private def on_preview_updated(preview)
13
+ @preview = preview
14
+ end
15
+
16
+ def evaluate(pageFunction, arg: nil)
17
+ if JavaScript.function?(pageFunction)
18
+ JavaScript::Function.new(pageFunction, arg).evaluate(@channel)
19
+ else
20
+ JavaScript::Expression.new(pageFunction).evaluate(@channel)
21
+ end
22
+ end
23
+
24
+ def evaluate_handle(pageFunction, arg: nil)
25
+ if JavaScript.function?(pageFunction)
26
+ JavaScript::Function.new(pageFunction, arg).evaluate_handle(@channel)
27
+ else
28
+ JavaScript::Expression.new(pageFunction).evaluate_handle(@channel)
29
+ end
30
+ end
31
+
32
+ def get_properties
33
+ resp = @channel.send_message_to_server('getPropertyList')
34
+ resp.map { |prop| [prop['name'], ChannelOwner.from(prop['value'])] }.to_h
35
+ end
36
+
37
+ def get_property(name)
38
+ resp = @channel.send_message_to_server('getProperty', name: name)
39
+ ChannelOwner.from(resp)
40
+ end
41
+
42
+ def as_element
43
+ nil
44
+ end
45
+
46
+ def dispose
47
+ @channel.send_message_to_server('dispose')
48
+ end
49
+
50
+ def json_value
51
+ value = @channel.send_message_to_server('jsonValue')
52
+ JavaScript::ValueParser.new(value).parse
53
+ end
54
+ end
55
+ end
@@ -3,26 +3,183 @@ require 'base64'
3
3
  module Playwright
4
4
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_page.py
5
5
  define_channel_owner :Page do
6
+ include Utils::Errors::SafeCloseError
6
7
  attr_writer :owned_context
7
8
 
8
9
  def after_initialize
10
+ @browser_context = @parent
11
+ @timeout_settings = TimeoutSettings.new(@browser_context.send(:_timeout_settings))
9
12
  @accessibility = Accessibility.new(@channel)
10
- @keyboard = Keyboard.new(@channel)
11
- @mouse = Mouse.new(@channel)
12
- @touchscreen = Touchscreen.new(@channel)
13
+ @keyboard = InputTypes::Keyboard.new(@channel)
14
+ @mouse = InputTypes::Mouse.new(@channel)
15
+ @touchscreen = InputTypes::Touchscreen.new(@channel)
13
16
 
17
+ @viewport_size = @initializer['viewportSize']
18
+ @closed = false
14
19
  @main_frame = ChannelOwners::Frame.from(@initializer['mainFrame'])
15
20
  @main_frame.send(:update_page_from_page, self)
16
21
  @frames = Set.new
17
22
  @frames << @main_frame
23
+
24
+ @channel.once('close', ->(_) { on_close })
25
+ @channel.on('console', ->(params) {
26
+ console_message = ChannelOwners::ConsoleMessage.from(params['message'])
27
+ emit(Events::Page::Console, console_message)
28
+ })
29
+ @channel.on('domcontentloaded', ->(_) { emit(Events::Page::DOMContentLoaded) })
30
+ @channel.on('frameAttached', ->(params) {
31
+ on_frame_attached(ChannelOwners::Frame.from(params['frame']))
32
+ })
33
+ @channel.on('frameDetached', ->(params) {
34
+ on_frame_detached(ChannelOwners::Frame.from(params['frame']))
35
+ })
36
+ @channel.on('load', ->(_) { emit(Events::Page::Load) })
37
+ @channel.on('popup', ->(params) {
38
+ emit(Events::Page::Popup, ChannelOwners::Page.from(params['page']))
39
+ })
40
+ end
41
+
42
+ attr_reader \
43
+ :accessibility,
44
+ :keyboard,
45
+ :mouse,
46
+ :touchscreen,
47
+ :viewport_size,
48
+ :main_frame
49
+
50
+ private def on_frame_attached(frame)
51
+ frame.send(:update_page_from_page, self)
52
+ @frames << frame
53
+ emit(Events::Page::FrameAttached, frame)
54
+ end
55
+
56
+ private def on_frame_detached(frame)
57
+ @frames.delete(frame)
58
+ frame.detached = true
59
+ emit(Events::Page::FrameDetached, frame)
60
+ end
61
+
62
+ private def on_close
63
+ @closed = true
64
+ @browser_context.send(:remove_page, self)
65
+ emit(Events::Page::Close)
66
+ end
67
+
68
+ def context
69
+ @browser_context
70
+ end
71
+
72
+ def opener
73
+ resp = @channel.send_message_to_server('opener')
74
+ ChannelOwners::Page.from(resp)
75
+ end
76
+
77
+ def frame(frameSelector)
78
+ name, url =
79
+ if frameSelector.is_a?(Hash)
80
+ [frameSelector[:name], frameSelector[:url]]
81
+ else
82
+ [frameSelector, nil]
83
+ end
84
+
85
+ if name
86
+ @frames.find { |f| f.name == name }
87
+ elsif url
88
+ # ref: https://github.com/microsoft/playwright-python/blob/c4320c27cb080b385a5e45be46baa3cb7a9409ff/playwright/_impl/_helper.py#L104
89
+ case url
90
+ when String
91
+ @frames.find { |f| f.url == url }
92
+ when Regexp
93
+ @frames.find { |f| url.match?(f.url) }
94
+ else
95
+ raise NotImplementedError.new('Page#frame with url is not completely implemented yet')
96
+ end
97
+ else
98
+ raise ArgumentError.new('Either name or url matcher should be specified')
99
+ end
100
+ end
101
+
102
+ def frames
103
+ @frames.to_a
104
+ end
105
+
106
+ def set_default_navigation_timeout(timeout)
107
+ @timeout_settings.default_navigation_timeout = timeout
108
+ @channel.send_message_to_server('setDefaultNavigationTimeoutNoReply', timeout: timeout)
109
+ end
110
+
111
+ def set_default_timeout(timeout)
112
+ @timeout_settings.default_timeout = timeout
113
+ @channel.send_message_to_server('setDefaultTimeoutNoReply', timeout: timeout)
114
+ end
115
+
116
+ def query_selector(selector)
117
+ @main_frame.query_selector(selector)
118
+ end
119
+
120
+ def query_selector_all(selector)
121
+ @main_frame.query_selector_all(selector)
122
+ end
123
+
124
+ def evaluate(pageFunction, arg: nil)
125
+ @main_frame.evaluate(pageFunction, arg: arg)
126
+ end
127
+
128
+ def evaluate_handle(pageFunction, arg: nil)
129
+ @main_frame.evaluate_handle(pageFunction, arg: arg)
130
+ end
131
+
132
+ def eval_on_selector(selector, pageFunction, arg: nil)
133
+ @main_frame.eval_on_selector(selector, pageFunction, arg: arg)
134
+ end
135
+
136
+ def eval_on_selector_all(selector, pageFunction, arg: nil)
137
+ @main_frame.eval_on_selector_all(selector, pageFunction, arg: arg)
18
138
  end
19
139
 
20
- attr_reader :accessibility, :keyboard, :mouse, :touchscreen, :main_frame
140
+ def add_script_tag(content: nil, path: nil, type: nil, url: nil)
141
+ @main_frame.add_script_tag(content: content, path: path, type: type, url: url)
142
+ end
143
+
144
+ def add_style_tag(content: nil, path: nil, url: nil)
145
+ @main_frame.add_style_tag(content: content, path: path, url: url)
146
+ end
147
+
148
+ def url
149
+ @main_frame.url
150
+ end
151
+
152
+ def content
153
+ @main_frame.content
154
+ end
155
+
156
+ def set_content(html, timeout: nil, waitUntil: nil)
157
+ @main_frame.set_content(html, timeout: timeout, waitUntil: waitUntil)
158
+ end
21
159
 
22
160
  def goto(url, timeout: nil, waitUntil: nil, referer: nil)
23
161
  @main_frame.goto(url, timeout: timeout, waitUntil: waitUntil, referer: referer)
24
162
  end
25
163
 
164
+ def reload(timeout: nil, waitUntil: nil)
165
+ params = {
166
+ timeout: timeout,
167
+ waitUntil: waitUntil,
168
+ }.compact
169
+ resp = @channel.send_message_to_server('reoad', params)
170
+ ChannelOwners::Response.from_nullable(resp)
171
+ end
172
+
173
+ def wait_for_load_state(state: nil, timeout: nil)
174
+ @main_frame.wait_for_load_state(state: state, timeout: timeout)
175
+ end
176
+
177
+ def set_viewport_size(viewportSize)
178
+ @viewport_size = viewportSize
179
+ @channel.send_message_to_server('setViewportSize', { viewportSize: viewportSize })
180
+ nil
181
+ end
182
+
26
183
  def screenshot(
27
184
  path: nil,
28
185
  type: nil,
@@ -41,7 +198,7 @@ module Playwright
41
198
  timeout: timeout,
42
199
  }.compact
43
200
  encoded_binary = @channel.send_message_to_server('screenshot', params)
44
- decoded_binary = Base64.decode64(encoded_binary)
201
+ decoded_binary = Base64.strict_decode64(encoded_binary)
45
202
  if path
46
203
  File.open(path, 'wb') do |f|
47
204
  f.write(decoded_binary)
@@ -49,5 +206,196 @@ module Playwright
49
206
  end
50
207
  decoded_binary
51
208
  end
209
+
210
+ def title
211
+ @main_frame.title
212
+ end
213
+
214
+ def close(runBeforeUnload: nil)
215
+ options = { runBeforeUnload: runBeforeUnload }.compact
216
+ @channel.send_message_to_server('close', options)
217
+ @owned_context&.close
218
+ nil
219
+ rescue => err
220
+ raise unless safe_close_error?(err)
221
+ end
222
+
223
+ def closed?
224
+ @closed
225
+ end
226
+
227
+ def click(
228
+ selector,
229
+ button: nil,
230
+ clickCount: nil,
231
+ delay: nil,
232
+ force: nil,
233
+ modifiers: nil,
234
+ noWaitAfter: nil,
235
+ position: nil,
236
+ timeout: nil)
237
+
238
+ @main_frame.click(
239
+ selector,
240
+ button: button,
241
+ clickCount: clickCount,
242
+ delay: delay,
243
+ force: force,
244
+ modifiers: modifiers,
245
+ noWaitAfter: noWaitAfter,
246
+ position: position,
247
+ timeout: timeout,
248
+ )
249
+ end
250
+
251
+ def dblclick(
252
+ selector,
253
+ button: nil,
254
+ delay: nil,
255
+ force: nil,
256
+ modifiers: nil,
257
+ noWaitAfter: nil,
258
+ position: nil,
259
+ timeout: nil)
260
+ @main_frame.dblclick(
261
+ selector,
262
+ button: button,
263
+ delay: delay,
264
+ force: force,
265
+ modifiers: modifiers,
266
+ noWaitAfter: noWaitAfter,
267
+ position: position,
268
+ timeout: timeout,
269
+ )
270
+ end
271
+
272
+ def fill(selector, value, noWaitAfter: nil, timeout: nil)
273
+ @main_frame.fill(selector, value, noWaitAfter: noWaitAfter, timeout: timeout)
274
+ end
275
+
276
+ def focus(selector, timeout: nil)
277
+ @main_frame.focus(selector, timeout: timeout)
278
+ end
279
+
280
+ def type(
281
+ selector,
282
+ text,
283
+ delay: nil,
284
+ noWaitAfter: nil,
285
+ timeout: nil)
286
+
287
+ @main_frame.type(selector, text, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
288
+ end
289
+
290
+ def press(
291
+ selector,
292
+ key,
293
+ delay: nil,
294
+ noWaitAfter: nil,
295
+ timeout: nil)
296
+
297
+ @main_frame.press(selector, key, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
298
+ end
299
+
300
+ def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
301
+ @main_frame.wait_for_function(pageFunction, arg: arg, polling: polling, timeout: timeout)
302
+ end
303
+
304
+ class CrashedError < StandardError
305
+ def initialize
306
+ super('Page crashed')
307
+ end
308
+ end
309
+
310
+ class AlreadyClosedError < StandardError
311
+ def initialize
312
+ super('Page closed')
313
+ end
314
+ end
315
+
316
+ class FrameAlreadyDetachedError < StandardError
317
+ def initialize
318
+ super('Navigating frame was detached!')
319
+ end
320
+ end
321
+
322
+ def expect_event(event, optionsOrPredicate: nil, &block)
323
+ predicate, timeout =
324
+ case optionsOrPredicate
325
+ when Proc
326
+ [optionsOrPredicate, nil]
327
+ when Hash
328
+ [optionsOrPredicate[:predicate], optionsOrPredicate[:timeout]]
329
+ else
330
+ [nil, nil]
331
+ end
332
+ timeout ||= @timeout_settings.timeout
333
+
334
+ wait_helper = WaitHelper.new
335
+ wait_helper.reject_on_timeout(timeout, "Timeout while waiting for event \"#{event}\"")
336
+
337
+ unless event == Events::Page::Crash
338
+ wait_helper.reject_on_event(self, Events::Page::Crash, CrashedError.new)
339
+ end
340
+
341
+ unless event == Events::Page::Close
342
+ wait_helper.reject_on_event(self, Events::Page::Close, AlreadyClosedError.new)
343
+ end
344
+
345
+ wait_helper.wait_for_event(self, event, predicate: predicate)
346
+
347
+ block&.call
348
+
349
+ wait_helper.promise.value!
350
+ end
351
+
352
+ def expect_navigation(timeout: nil, url: nil, waitUntil: nil, &block)
353
+ @main_frame.expect_navigation(
354
+ timeout: timeout,
355
+ url: url,
356
+ waitUntil: waitUntil,
357
+ &block)
358
+ end
359
+
360
+ def expect_request(urlOrPredicate, timeout: nil)
361
+ predicate =
362
+ case urlOrPredicate
363
+ when String, Regexp
364
+ url_matcher = UrlMatcher.new(urlOrPredicate)
365
+ -> (req){ url_matcher.match?(req.url) }
366
+ when Proc
367
+ urlOrPredicate
368
+ else
369
+ -> (_) { true }
370
+ end
371
+
372
+ expect_event(Events::Page::Request, optionsOrPredicate: { predicate: predicate, timeout: timeout})
373
+ end
374
+
375
+ def expect_response(urlOrPredicate, timeout: nil)
376
+ predicate =
377
+ case urlOrPredicate
378
+ when String, Regexp
379
+ url_matcher = UrlMatcher.new(urlOrPredicate)
380
+ -> (req){ url_matcher.match?(req.url) }
381
+ when Proc
382
+ urlOrPredicate
383
+ else
384
+ -> (_) { true }
385
+ end
386
+
387
+ expect_event(Events::Page::Response, optionsOrPredicate: { predicate: predicate, timeout: timeout})
388
+ end
389
+
390
+ # called from BrowserContext#on_page with send(:update_browser_context, page), so keep private.
391
+ private def update_browser_context(context)
392
+ @browser_context = context
393
+ @timeout_settings = TimeoutSettings.new(context.send(:_timeout_settings))
394
+ end
395
+
396
+ # called from Frame with send(:timeout_settings)
397
+ private def timeout_settings
398
+ @timeout_settings
399
+ end
52
400
  end
53
401
  end
@@ -1,5 +1,95 @@
1
+ require 'base64'
2
+
1
3
  module Playwright
2
4
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_network.py
3
5
  define_channel_owner :Request do
6
+ def after_initialize
7
+ @redirected_from = ChannelOwners::Request.from_nullable(@initializer['redirectedFrom'])
8
+ @redirected_from&.send(:update_redirected_to, self)
9
+ @timing = {
10
+ startTime: 0,
11
+ domainLookupStart: -1,
12
+ domainLookupEnd: -1,
13
+ connectStart: -1,
14
+ secureConnectionStart: -1,
15
+ connectEnd: -1,
16
+ requestStart: -1,
17
+ responseStart: -1,
18
+ responseEnd: -1,
19
+ }
20
+ @headers = parse_headers(@initializer['headers'])
21
+ end
22
+
23
+ def url
24
+ @initializer['url']
25
+ end
26
+
27
+ def resource_type
28
+ @initializer['resourceType']
29
+ end
30
+
31
+ def method
32
+ @initialize['method']
33
+ end
34
+
35
+ def post_data
36
+ post_data_buffer
37
+ end
38
+
39
+ def post_data_json
40
+ data = post_data
41
+ return unless data
42
+
43
+ content_type = @headers['content-type']
44
+ return unless content_type
45
+
46
+ if content_type == "application/x-www-form-urlencoded"
47
+ URI.decode_www_form(data).to_h
48
+ else
49
+ JSON.parse(data)
50
+ end
51
+ end
52
+
53
+ def post_data_buffer
54
+ base64_content = @initializer['postData']
55
+ if base64_content
56
+ Base64.strict_decode64(base64_content)
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ def headers
63
+ @headers
64
+ end
65
+
66
+ def response
67
+ resp = @channel.send_message_to_server('response')
68
+ ChannelOwners::Response.from_nullable(resp)
69
+ end
70
+
71
+ def frame
72
+ @initializer['frame']
73
+ end
74
+
75
+ def navigation_request?
76
+ @initializer['isNavigationRequest']
77
+ end
78
+
79
+ def failure
80
+ @failure_text
81
+ end
82
+
83
+ attr_reader :headers, :redirected_from, :redirected_to, :timing
84
+
85
+ private def update_redirected_to(request)
86
+ @redirected_to = request
87
+ end
88
+
89
+ private def parse_headers(headers)
90
+ headers.map do |header|
91
+ [header['name'].downcase, header['value']]
92
+ end.to_h
93
+ end
4
94
  end
5
95
  end