playwright-ruby-client 0.0.3 → 0.0.8

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 (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