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
@@ -2,9 +2,8 @@ module Playwright
2
2
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_frame.py
3
3
  define_channel_owner :Frame do
4
4
  def after_initialize
5
- @event_emitter = Object.new.extend(EventEmitter)
6
5
  if @initializer['parentFrame']
7
- @parent_frame = self.from(@initializer['parentFrame'])
6
+ @parent_frame = ChannelOwners::Frame.from(@initializer['parentFrame'])
8
7
  @parent_frame.send(:append_child_frame_from_child, self)
9
8
  end
10
9
  @name = @initializer['name']
@@ -12,9 +11,36 @@ module Playwright
12
11
  @detached = false
13
12
  @child_frames = Set.new
14
13
  @load_states = Set.new(@initializer['loadStates'])
14
+ @event_emitter = Object.new.extend(EventEmitter)
15
+
16
+ @channel.on('loadstate', ->(params) {
17
+ on_load_state(add: params['add'], remove: params['remove'])
18
+ })
19
+ @channel.on('navigated', method(:on_frame_navigated))
20
+ end
21
+
22
+ attr_reader :page, :parent_frame
23
+ attr_writer :detached
24
+
25
+ private def on_load_state(add:, remove:)
26
+ if add
27
+ @load_states << add
28
+ @event_emitter.emit('loadstate', add)
29
+ end
30
+ if remove
31
+ @load_states.delete(remove)
32
+ end
15
33
  end
16
34
 
17
- attr_reader :page
35
+ private def on_frame_navigated(event)
36
+ @url = event['url']
37
+ @name = event['name']
38
+ @event_emitter.emit('navigated', event)
39
+
40
+ unless event['error']
41
+ @page&.emit('framenavigated', self)
42
+ end
43
+ end
18
44
 
19
45
  def goto(url, timeout: nil, waitUntil: nil, referer: nil)
20
46
  params = {
@@ -24,20 +50,453 @@ module Playwright
24
50
  referer: referer
25
51
  }.compact
26
52
  resp = @channel.send_message_to_server('goto', params)
27
- ChannelOwners::Response.from(resp)
53
+ ChannelOwners::Response.from_nullable(resp)
54
+ end
55
+
56
+ private def setup_navigation_wait_helper(timeout:)
57
+ WaitHelper.new.tap do |helper|
58
+ helper.reject_on_event(@page, Events::Page::Close, AlreadyClosedError.new)
59
+ helper.reject_on_event(@page, Events::Page::Crash, CrashedError.new)
60
+ helper.reject_on_event(@page, Events::Page::FrameDetached, FrameAlreadyDetachedError.new)
61
+ helper.reject_on_timeout(timeout, "Timeout #{timeout}ms exceeded.")
62
+ end
63
+ end
64
+
65
+ def expect_navigation(timeout: nil, url: nil, waitUntil: nil, &block)
66
+ option_wait_until = waitUntil || 'load'
67
+ option_timeout = timeout || @page.send(:timeout_settings).navigation_timeout
68
+ time_start = Time.now
69
+
70
+ wait_helper = setup_navigation_wait_helper(timeout: option_timeout)
71
+
72
+ predicate =
73
+ if url
74
+ matcher = UrlMatcher.new(url)
75
+ ->(event) { event['error'] || matcher.match?(event['url']) }
76
+ else
77
+ ->(_) { true }
78
+ end
79
+
80
+ wait_helper.wait_for_event(@event_emitter, 'navigated', predicate: predicate)
81
+
82
+ block&.call
83
+
84
+ event = wait_helper.promise.value!
85
+ if event['error']
86
+ raise event['error']
87
+ end
88
+
89
+ unless @load_states.include?(option_wait_until)
90
+ elapsed_time = Time.now - time_start
91
+ if elapsed_time < option_timeout
92
+ wait_for_load_state(state: option_wait_until, timeout: option_timeout - elapsed_time)
93
+ end
94
+ end
95
+
96
+ request_json = event.dig('newDocument', 'request')
97
+ request = ChannelOwners::Request.from_nullable(request_json)
98
+ request&.response
99
+ end
100
+
101
+ def wait_for_load_state(state: nil, timeout: nil)
102
+ option_state = state || 'load'
103
+ option_timeout = timeout || @page.send(:timeout_settings).navigation_timeout
104
+ unless %w(load domcontentloaded networkidle).include?(option_state)
105
+ raise ArgumentError.new('state: expected one of (load|domcontentloaded|networkidle)')
106
+ end
107
+ if @load_states.include?(option_state)
108
+ return
109
+ end
110
+
111
+ wait_helper = setup_navigation_wait_helper(timeout: option_timeout)
112
+
113
+ predicate = ->(state) { state == option_state }
114
+ wait_helper.wait_for_event(@event_emitter, 'loadstate', predicate: predicate)
115
+ wait_helper.promise.value!
116
+
117
+ nil
118
+ end
119
+
120
+ def evaluate(pageFunction, arg: nil)
121
+ if JavaScript.function?(pageFunction)
122
+ JavaScript::Function.new(pageFunction, arg).evaluate(@channel)
123
+ else
124
+ JavaScript::Expression.new(pageFunction).evaluate(@channel)
125
+ end
126
+ end
127
+
128
+ def evaluate_handle(pageFunction, arg: nil)
129
+ if JavaScript.function?(pageFunction)
130
+ JavaScript::Function.new(pageFunction, arg).evaluate_handle(@channel)
131
+ else
132
+ JavaScript::Expression.new(pageFunction).evaluate_handle(@channel)
133
+ end
134
+ end
135
+
136
+ def query_selector(selector)
137
+ resp = @channel.send_message_to_server('querySelector', selector: selector)
138
+ ChannelOwners::ElementHandle.from_nullable(resp)
139
+ end
140
+
141
+ def query_selector_all(selector)
142
+ @channel.send_message_to_server('querySelectorAll', selector: selector).map do |el|
143
+ ChannelOwners::ElementHandle.from(el)
144
+ end
145
+ end
146
+
147
+ def checked?(selector, timeout: nil)
148
+ params = { selector: selector, timeout: timeout }.compact
149
+ @channel.send_message_to_server('isChecked', params)
150
+ end
151
+
152
+ def disabled?(selector, timeout: nil)
153
+ params = { selector: selector, timeout: timeout }.compact
154
+ @channel.send_message_to_server('isDisabled', params)
155
+ end
156
+
157
+ def editable?(selector, timeout: nil)
158
+ params = { selector: selector, timeout: timeout }.compact
159
+ @channel.send_message_to_server('isEditable', params)
160
+ end
161
+
162
+ def enabled?(selector, timeout: nil)
163
+ params = { selector: selector, timeout: timeout }.compact
164
+ @channel.send_message_to_server('isEnabled', params)
165
+ end
166
+
167
+ def hidden?(selector, timeout: nil)
168
+ params = { selector: selector, timeout: timeout }.compact
169
+ @channel.send_message_to_server('isHidden', params)
170
+ end
171
+
172
+ def visible?(selector, timeout: nil)
173
+ params = { selector: selector, timeout: timeout }.compact
174
+ @channel.send_message_to_server('isVisible', params)
175
+ end
176
+
177
+ def dispatch_event(selector, type, eventInit: nil, timeout: nil)
178
+ params = {
179
+ selector: selector,
180
+ type: type,
181
+ eventInit: JavaScript::ValueSerializer.new(eventInit).serialize,
182
+ timeout: timeout,
183
+ }.compact
184
+ @channel.send_message_to_server('dispatchEvent', params)
185
+
186
+ nil
187
+ end
188
+
189
+ def eval_on_selector(selector, pageFunction, arg: nil)
190
+ if JavaScript.function?(pageFunction)
191
+ JavaScript::Function.new(pageFunction, arg).eval_on_selector(@channel, selector)
192
+ else
193
+ JavaScript::Expression.new(pageFunction).eval_on_selector(@channel, selector)
194
+ end
195
+ end
196
+
197
+ def eval_on_selector_all(selector, pageFunction, arg: nil)
198
+ if JavaScript.function?(pageFunction)
199
+ JavaScript::Function.new(pageFunction, arg).eval_on_selector_all(@channel, selector)
200
+ else
201
+ JavaScript::Expression.new(pageFunction).eval_on_selector_all(@channel, selector)
202
+ end
203
+ end
204
+
205
+ def content
206
+ @channel.send_message_to_server('content')
207
+ end
208
+
209
+ def set_content(html, timeout: nil, waitUntil: nil)
210
+ params = {
211
+ html: html,
212
+ timeout: timeout,
213
+ waitUntil: waitUntil,
214
+ }.compact
215
+
216
+ @channel.send_message_to_server('setContent', params)
217
+
218
+ nil
219
+ end
220
+
221
+ def name
222
+ @name || ''
223
+ end
224
+
225
+ def url
226
+ @url || ''
227
+ end
228
+
229
+ def child_frames
230
+ @child_frames.to_a
231
+ end
232
+
233
+ def detached?
234
+ @detached
235
+ end
236
+
237
+ def add_script_tag(content: nil, path: nil, type: nil, url: nil)
238
+ params = {
239
+ content: content,
240
+ type: type,
241
+ url: url,
242
+ }.compact
243
+ if path
244
+ params[:content] = "#{File.read(path)}\n//# sourceURL=#{path}"
245
+ end
246
+ resp = @channel.send_message_to_server('addScriptTag', params)
247
+ ChannelOwners::ElementHandle.from(resp)
248
+ end
249
+
250
+ def add_style_tag(content: nil, path: nil, url: nil)
251
+ params = {
252
+ content: content,
253
+ url: url,
254
+ }.compact
255
+ if path
256
+ params[:content] = "#{File.read(path)}\n/*# sourceURL=#{path}*/"
257
+ end
258
+ resp = @channel.send_message_to_server('addStyleTag', params)
259
+ ChannelOwners::ElementHandle.from(resp)
260
+ end
261
+
262
+ def click(
263
+ selector,
264
+ button: nil,
265
+ clickCount: nil,
266
+ delay: nil,
267
+ force: nil,
268
+ modifiers: nil,
269
+ noWaitAfter: nil,
270
+ position: nil,
271
+ timeout: nil)
272
+
273
+ params = {
274
+ selector: selector,
275
+ button: button,
276
+ clickCount: clickCount,
277
+ delay: delay,
278
+ force: force,
279
+ modifiers: modifiers,
280
+ noWaitAfter: noWaitAfter,
281
+ position: position,
282
+ timeout: timeout,
283
+ }.compact
284
+ @channel.send_message_to_server('click', params)
285
+
286
+ nil
287
+ end
288
+
289
+ def dblclick(
290
+ selector,
291
+ button: nil,
292
+ delay: nil,
293
+ force: nil,
294
+ modifiers: nil,
295
+ noWaitAfter: nil,
296
+ position: nil,
297
+ timeout: nil)
298
+
299
+ params = {
300
+ selector: selector,
301
+ button: button,
302
+ delay: delay,
303
+ force: force,
304
+ modifiers: modifiers,
305
+ noWaitAfter: noWaitAfter,
306
+ position: position,
307
+ timeout: timeout,
308
+ }.compact
309
+ @channel.send_message_to_server('dblclick', params)
310
+
311
+ nil
312
+ end
313
+
314
+ def tap_point(
315
+ selector,
316
+ force: nil,
317
+ modifiers: nil,
318
+ noWaitAfter: nil,
319
+ position: nil,
320
+ timeout: nil)
321
+ params = {
322
+ selector: selector,
323
+ force: force,
324
+ modifiers: modifiers,
325
+ noWaitAfter: noWaitAfter,
326
+ position: position,
327
+ timeout: timeout,
328
+ }.compact
329
+ @channel.send_message_to_server('tap', params)
330
+
331
+ nil
332
+ end
333
+
334
+ def fill(selector, value, noWaitAfter: nil, timeout: nil)
335
+ params = {
336
+ selector: selector,
337
+ value: value,
338
+ noWaitAfter: noWaitAfter,
339
+ timeout: timeout,
340
+ }.compact
341
+ @channel.send_message_to_server('fill', params)
342
+
343
+ nil
344
+ end
345
+
346
+ def focus(selector, timeout: nil)
347
+ params = { selector: selector, timeout: timeout }.compact
348
+ @channel.send_message_to_server('focus', params)
349
+ nil
350
+ end
351
+
352
+ def text_content(selector, timeout: nil)
353
+ params = { selector: selector, timeout: timeout }.compact
354
+ @channel.send_message_to_server('textContent', params)
355
+ end
356
+
357
+ def inner_text(selector, timeout: nil)
358
+ params = { selector: selector, timeout: timeout }.compact
359
+ @channel.send_message_to_server('innerText', params)
360
+ end
361
+
362
+ def inner_html(selector, timeout: nil)
363
+ params = { selector: selector, timeout: timeout }.compact
364
+ @channel.send_message_to_server('innerHTML', params)
365
+ end
366
+
367
+ def get_attribute(selector, name, timeout: nil)
368
+ params = {
369
+ selector: selector,
370
+ name: name,
371
+ timeout: timeout,
372
+ }.compact
373
+ @channel.send_message_to_server('getAttribute', params)
374
+ end
375
+
376
+ def hover(
377
+ selector,
378
+ force: nil,
379
+ modifiers: nil,
380
+ position: nil,
381
+ timeout: nil)
382
+ params = {
383
+ selector: selector,
384
+ force: force,
385
+ modifiers: modifiers,
386
+ position: position,
387
+ timeout: timeout,
388
+ }.compact
389
+ @channel.send_message_to_server('hover', params)
390
+
391
+ nil
392
+ end
393
+
394
+ def select_option(selector, values, noWaitAfter: nil, timeout: nil)
395
+ base_params = SelectOptionValues.new(values).as_params
396
+ params = base_params + { selector: selector, noWaitAfter: noWaitAfter, timeout: timeout }.compact
397
+ @channel.send_message_to_server('selectOption', params)
398
+
399
+ nil
28
400
  end
29
401
 
30
- private
402
+ def set_input_files(selector, files, noWaitAfter: nil, timeout: nil)
403
+ base_params = InputFiles.new(values).as_params
404
+ params = base_params + { selector: selector, noWaitAfter: noWaitAfter, timeout: timeout }.compact
405
+ @channel.send_message_to_server('setInputFiles', params)
406
+
407
+ nil
408
+ end
409
+
410
+ def type(
411
+ selector,
412
+ text,
413
+ delay: nil,
414
+ noWaitAfter: nil,
415
+ timeout: nil)
416
+
417
+ params = {
418
+ selector: selector,
419
+ text: text,
420
+ delay: delay,
421
+ noWaitAfter: noWaitAfter,
422
+ timeout: timeout,
423
+ }.compact
424
+ @channel.send_message_to_server('type', params)
425
+
426
+ nil
427
+ end
428
+
429
+ def press(
430
+ selector,
431
+ key,
432
+ delay: nil,
433
+ noWaitAfter: nil,
434
+ timeout: nil)
435
+
436
+ params = {
437
+ selector: selector,
438
+ key: key,
439
+ delay: delay,
440
+ noWaitAfter: noWaitAfter,
441
+ timeout: timeout,
442
+ }.compact
443
+ @channel.send_message_to_server('press', params)
444
+
445
+ nil
446
+ end
447
+
448
+ def check(selector, force: nil, noWaitAfter: nil, timeout: nil)
449
+ params = {
450
+ selector: selector,
451
+ force: force,
452
+ noWaitAfter: noWaitAfter,
453
+ timeout: timeout,
454
+ }.compact
455
+ @channel.send_message_to_server('check', params)
456
+
457
+ nil
458
+ end
459
+
460
+ def uncheck(selector, force: nil, noWaitAfter: nil, timeout: nil)
461
+ params = {
462
+ selector: selector,
463
+ force: force,
464
+ noWaitAfter: noWaitAfter,
465
+ timeout: timeout,
466
+ }.compact
467
+ @channel.send_message_to_server('uncheck', params)
468
+
469
+ nil
470
+ end
471
+
472
+ def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
473
+ if polling.is_a?(String) && polling != 'raf'
474
+ raise ArgumentError.new("Unknown polling option: #{polling}")
475
+ end
476
+
477
+ function_or_expression =
478
+ if JavaScript.function?(pageFunction)
479
+ JavaScript::Function.new(pageFunction, arg)
480
+ else
481
+ JavaScript::Expression.new(pageFunction)
482
+ end
483
+
484
+ function_or_expression.wait_for_function(@channel, polling: polling, timeout: timeout)
485
+ end
486
+
487
+ def title
488
+ @channel.send_message_to_server('title')
489
+ end
31
490
 
32
491
  # @param page [Page]
33
492
  # @note This method should be used internally. Accessed via .send method, so keep private!
34
- def update_page_from_page(page)
493
+ private def update_page_from_page(page)
35
494
  @page = page
36
495
  end
37
496
 
38
497
  # @param child [Frame]
39
498
  # @note This method should be used internally. Accessed via .send method, so keep private!
40
- def append_child_frame_from_child(frame)
499
+ private def append_child_frame_from_child(frame)
41
500
  @child_frames << frame
42
501
  end
43
502
  end