playwright-ruby-client 0.2.1 → 0.5.5

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -5
  3. data/docs/api_coverage.md +116 -74
  4. data/lib/playwright.rb +48 -9
  5. data/lib/playwright/channel.rb +12 -2
  6. data/lib/playwright/channel_owners/artifact.rb +30 -0
  7. data/lib/playwright/channel_owners/binding_call.rb +3 -0
  8. data/lib/playwright/channel_owners/browser.rb +21 -0
  9. data/lib/playwright/channel_owners/browser_context.rb +154 -3
  10. data/lib/playwright/channel_owners/browser_type.rb +28 -0
  11. data/lib/playwright/channel_owners/dialog.rb +28 -0
  12. data/lib/playwright/channel_owners/element_handle.rb +7 -7
  13. data/lib/playwright/channel_owners/frame.rb +26 -5
  14. data/lib/playwright/channel_owners/js_handle.rb +2 -2
  15. data/lib/playwright/channel_owners/page.rb +125 -25
  16. data/lib/playwright/channel_owners/playwright.rb +24 -27
  17. data/lib/playwright/channel_owners/request.rb +26 -2
  18. data/lib/playwright/channel_owners/response.rb +60 -0
  19. data/lib/playwright/channel_owners/route.rb +78 -0
  20. data/lib/playwright/channel_owners/selectors.rb +19 -1
  21. data/lib/playwright/channel_owners/stream.rb +15 -0
  22. data/lib/playwright/connection.rb +11 -32
  23. data/lib/playwright/download.rb +27 -0
  24. data/lib/playwright/errors.rb +6 -0
  25. data/lib/playwright/events.rb +2 -5
  26. data/lib/playwright/keyboard_impl.rb +1 -1
  27. data/lib/playwright/mouse_impl.rb +41 -0
  28. data/lib/playwright/playwright_api.rb +3 -1
  29. data/lib/playwright/route_handler_entry.rb +28 -0
  30. data/lib/playwright/select_option_values.rb +31 -22
  31. data/lib/playwright/transport.rb +29 -7
  32. data/lib/playwright/url_matcher.rb +1 -1
  33. data/lib/playwright/utils.rb +9 -0
  34. data/lib/playwright/version.rb +1 -1
  35. data/lib/playwright/video.rb +51 -0
  36. data/lib/playwright/wait_helper.rb +2 -2
  37. data/lib/playwright_api/accessibility.rb +39 -1
  38. data/lib/playwright_api/android.rb +74 -2
  39. data/lib/playwright_api/android_device.rb +141 -23
  40. data/lib/playwright_api/android_input.rb +17 -13
  41. data/lib/playwright_api/android_socket.rb +16 -0
  42. data/lib/playwright_api/android_web_view.rb +21 -0
  43. data/lib/playwright_api/browser.rb +77 -2
  44. data/lib/playwright_api/browser_context.rb +178 -25
  45. data/lib/playwright_api/browser_type.rb +40 -9
  46. data/lib/playwright_api/dialog.rb +54 -7
  47. data/lib/playwright_api/element_handle.rb +105 -31
  48. data/lib/playwright_api/file_chooser.rb +6 -1
  49. data/lib/playwright_api/frame.rb +229 -36
  50. data/lib/playwright_api/js_handle.rb +23 -0
  51. data/lib/playwright_api/keyboard.rb +48 -1
  52. data/lib/playwright_api/mouse.rb +26 -5
  53. data/lib/playwright_api/page.rb +491 -81
  54. data/lib/playwright_api/playwright.rb +21 -4
  55. data/lib/playwright_api/request.rb +30 -2
  56. data/lib/playwright_api/response.rb +21 -11
  57. data/lib/playwright_api/route.rb +51 -5
  58. data/lib/playwright_api/selectors.rb +27 -1
  59. data/lib/playwright_api/touchscreen.rb +1 -1
  60. data/lib/playwright_api/worker.rb +25 -1
  61. data/playwright.gemspec +4 -2
  62. metadata +42 -14
  63. data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
  64. data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
  65. data/lib/playwright/channel_owners/download.rb +0 -27
  66. data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
  67. data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
  68. data/lib/playwright_api/binding_call.rb +0 -27
  69. data/lib/playwright_api/chromium_browser_context.rb +0 -59
  70. data/lib/playwright_api/download.rb +0 -95
  71. data/lib/playwright_api/video.rb +0 -24
@@ -9,8 +9,8 @@ module Playwright
9
9
  @preview
10
10
  end
11
11
 
12
- private def on_preview_updated(preview)
13
- @preview = preview
12
+ private def on_preview_updated(params)
13
+ @preview = params['preview']
14
14
  end
15
15
 
16
16
  def evaluate(pageFunction, arg: nil)
@@ -14,12 +14,21 @@ module Playwright
14
14
  @mouse = MouseImpl.new(@channel)
15
15
  @touchscreen = TouchscreenImpl.new(@channel)
16
16
 
17
- @viewport_size = @initializer['viewportSize']
17
+ if @initializer['viewportSize']
18
+ @viewport_size = {
19
+ width: @initializer['viewportSize']['width'],
20
+ height: @initializer['viewportSize']['height'],
21
+ }
22
+ end
18
23
  @closed = false
24
+ @bindings = {}
25
+ @routes = Set.new
26
+
19
27
  @main_frame = ChannelOwners::Frame.from(@initializer['mainFrame'])
20
28
  @main_frame.send(:update_page_from_page, self)
21
29
  @frames = Set.new
22
30
  @frames << @main_frame
31
+ @opener = ChannelOwners::Page.from_nullable(@initializer['opener'])
23
32
 
24
33
  @channel.once('close', ->(_) { on_close })
25
34
  @channel.on('console', ->(params) {
@@ -29,9 +38,7 @@ module Playwright
29
38
  @channel.on('crash', ->(_) { emit(Events::Page::Crash) })
30
39
  @channel.on('dialog', method(:on_dialog))
31
40
  @channel.on('domcontentloaded', ->(_) { emit(Events::Page::DOMContentLoaded) })
32
- @channel.on('download', ->(params) {
33
- emit(Events::Page::Download, ChannelOwners::Download.from(params['download']))
34
- })
41
+ @channel.on('download', method(:on_download))
35
42
  @channel.on('fileChooser', ->(params) {
36
43
  chooser = FileChooserImpl.new(
37
44
  page: self,
@@ -49,9 +56,6 @@ module Playwright
49
56
  @channel.on('pageError', ->(params) {
50
57
  emit(Events::Page::PageError, Error.parse(params['error']['error']))
51
58
  })
52
- @channel.on('popup', ->(params) {
53
- emit(Events::Page::Popup, ChannelOwners::Page.from(params['page']))
54
- })
55
59
  @channel.on('request', ->(params) {
56
60
  emit(Events::Page::Request, ChannelOwners::Request.from(params['request']))
57
61
  })
@@ -74,9 +78,7 @@ module Playwright
74
78
  @channel.on('route', ->(params) {
75
79
  on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
76
80
  })
77
- @channel.on('video', ->(params) {
78
- video.send(:update_relative_path, params['relativePath'])
79
- })
81
+ @channel.on('video', method(:on_video))
80
82
  @channel.on('webSocket', ->(params) {
81
83
  emit(Events::Page::WebSocket, ChannelOwners::WebSocket.from(params['webSocket']))
82
84
  })
@@ -96,12 +98,12 @@ module Playwright
96
98
  private def on_request_failed(request, response_end_timing, failure_text)
97
99
  request.send(:update_failure_text, failure_text)
98
100
  request.send(:update_response_end_timing, response_end_timing)
99
- emit(Events::Page::RequestFailed)
101
+ emit(Events::Page::RequestFailed, request)
100
102
  end
101
103
 
102
104
  private def on_request_finished(request, response_end_timing)
103
105
  request.send(:update_response_end_timing, response_end_timing)
104
- emit(Events::Page::RequestFinished)
106
+ emit(Events::Page::RequestFinished, request)
105
107
  end
106
108
 
107
109
  private def on_frame_attached(frame)
@@ -117,8 +119,15 @@ module Playwright
117
119
  end
118
120
 
119
121
  private def on_route(route, request)
120
- # @routes.each ...
121
- @browser_context.send(:on_route, route, request)
122
+ # It is not desired to use PlaywrightApi.wrap directly.
123
+ # However it is a little difficult to define wrapper for `handler` parameter in generate_api.
124
+ # Just a workaround...
125
+ wrapped_route = PlaywrightApi.wrap(route)
126
+ wrapped_request = PlaywrightApi.wrap(request)
127
+
128
+ if @routes.none? { |handler_entry| handler_entry.handle(wrapped_route, wrapped_request) }
129
+ @browser_context.send(:on_route, route, request)
130
+ end
122
131
  end
123
132
 
124
133
  private def on_close
@@ -130,14 +139,28 @@ module Playwright
130
139
  private def on_dialog(params)
131
140
  dialog = ChannelOwners::Dialog.from(params['dialog'])
132
141
  unless emit(Events::Page::Dialog, dialog)
133
- dialog.dismiss # FIXME: this should be asynchronous
142
+ dialog.dismiss
134
143
  end
135
144
  end
136
145
 
146
+ private def on_download(params)
147
+ download = Download.new(
148
+ url: params['url'],
149
+ suggested_filename: params['suggestedFilename'],
150
+ artifact: ChannelOwners::Artifact.from(params['artifact']),
151
+ )
152
+ emit(Events::Page::Download, download)
153
+ end
154
+
155
+ private def on_video(params)
156
+ artifact = ChannelOwners::Artifact.from(params['artifact'])
157
+ video.send(:set_artifact, artifact)
158
+ end
159
+
137
160
  # @override
138
161
  def on(event, callback)
139
162
  if event == Events::Page::FileChooser && listener_count(event) == 0
140
- @channel.send_no_reply('setFileChooserInterceptedNoReply', intercepted: true)
163
+ @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: true)
141
164
  end
142
165
  super
143
166
  end
@@ -145,7 +168,7 @@ module Playwright
145
168
  # @override
146
169
  def once(event, callback)
147
170
  if event == Events::Page::FileChooser && listener_count(event) == 0
148
- @channel.send_no_reply('setFileChooserInterceptedNoReply', intercepted: true)
171
+ @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: true)
149
172
  end
150
173
  super
151
174
  end
@@ -154,7 +177,7 @@ module Playwright
154
177
  def off(event, callback)
155
178
  super
156
179
  if event == Events::Page::FileChooser && listener_count(event) == 0
157
- @channel.send_no_reply('setFileChooserInterceptedNoReply', intercepted: false)
180
+ @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: false)
158
181
  end
159
182
  end
160
183
 
@@ -163,8 +186,17 @@ module Playwright
163
186
  end
164
187
 
165
188
  def opener
166
- resp = @channel.send_message_to_server('opener')
167
- ChannelOwners::Page.from(resp)
189
+ if @opener&.closed?
190
+ nil
191
+ else
192
+ @opener
193
+ end
194
+ end
195
+
196
+ private def emit_popup_event_from_browser_context
197
+ if @opener && !@opener.closed?
198
+ @opener.emit(Events::Page::Popup, self)
199
+ end
168
200
  end
169
201
 
170
202
  def frame(name: nil, url: nil)
@@ -311,6 +343,10 @@ module Playwright
311
343
  @main_frame.wait_for_load_state(state: state, timeout: timeout)
312
344
  end
313
345
 
346
+ def wait_for_url(url, timeout: nil, waitUntil: nil)
347
+ @main_frame.wait_for_url(url, timeout: timeout, waitUntil: waitUntil)
348
+ end
349
+
314
350
  def go_back(timeout: nil, waitUntil: nil)
315
351
  params = { timeout: timeout, waitUntil: waitUntil }.compact
316
352
  resp = @channel.send_message_to_server('goBack', params)
@@ -347,7 +383,7 @@ module Playwright
347
383
  def add_init_script(path: nil, script: nil)
348
384
  source =
349
385
  if path
350
- File.read(path, 'r')
386
+ File.read(path)
351
387
  elsif script
352
388
  script
353
389
  else
@@ -358,6 +394,23 @@ module Playwright
358
394
  nil
359
395
  end
360
396
 
397
+ def route(url, handler)
398
+ entry = RouteHandlerEntry.new(url, handler)
399
+ @routes << entry
400
+ if @routes.count >= 1
401
+ @channel.send_message_to_server('setNetworkInterceptionEnabled', enabled: true)
402
+ end
403
+ end
404
+
405
+ def unroute(url, handler: nil)
406
+ @routes.reject! do |handler_entry|
407
+ handler_entry.same_value?(url: url, handler: handler)
408
+ end
409
+ if @routes.count == 0
410
+ @channel.send_message_to_server('setNetworkInterceptionEnabled', enabled: false)
411
+ end
412
+ end
413
+
361
414
  def screenshot(
362
415
  path: nil,
363
416
  type: nil,
@@ -546,12 +599,24 @@ module Playwright
546
599
  @main_frame.press(selector, key, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
547
600
  end
548
601
 
549
- def check(selector, force: nil, noWaitAfter: nil, timeout: nil)
550
- @main_frame.check(selector, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
602
+ def check(
603
+ selector,
604
+ force: nil,
605
+ noWaitAfter: nil,
606
+ position: nil,
607
+ timeout: nil)
608
+
609
+ @main_frame.check(selector, force: force, noWaitAfter: noWaitAfter, position: position, timeout: timeout)
551
610
  end
552
611
 
553
- def uncheck(selector, force: nil, noWaitAfter: nil, timeout: nil)
554
- @main_frame.uncheck(selector, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
612
+ def uncheck(
613
+ selector,
614
+ force: nil,
615
+ noWaitAfter: nil,
616
+ position: nil,
617
+ timeout: nil)
618
+
619
+ @main_frame.uncheck(selector, force: force, noWaitAfter: noWaitAfter, position: position, timeout: timeout)
555
620
  end
556
621
 
557
622
  def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
@@ -597,6 +662,36 @@ module Playwright
597
662
  decoded_binary
598
663
  end
599
664
 
665
+ def video
666
+ return nil unless @browser_context.send(:has_record_video_option?)
667
+ @video ||= Video.new(self)
668
+ end
669
+
670
+ def start_js_coverage(resetOnNavigation: nil, reportAnonymousScripts: nil)
671
+ params = {
672
+ resetOnNavigation: resetOnNavigation,
673
+ reportAnonymousScripts: reportAnonymousScripts,
674
+ }.compact
675
+
676
+ @channel.send_message_to_server('startJSCoverage', params)
677
+ end
678
+
679
+ def stop_js_coverage
680
+ @channel.send_message_to_server('stopJSCoverage')
681
+ end
682
+
683
+ def start_css_coverage(resetOnNavigation: nil, reportAnonymousScripts: nil)
684
+ params = {
685
+ resetOnNavigation: resetOnNavigation,
686
+ }.compact
687
+
688
+ @channel.send_message_to_server('startCSSCoverage', params)
689
+ end
690
+
691
+ def stop_css_coverage
692
+ @channel.send_message_to_server('stopCSSCoverage')
693
+ end
694
+
600
695
  class CrashedError < StandardError
601
696
  def initialize
602
697
  super('Page crashed')
@@ -698,5 +793,10 @@ module Playwright
698
793
  private def timeout_settings
699
794
  @timeout_settings
700
795
  end
796
+
797
+ # called from BrowserContext#expose_binding
798
+ private def has_bindings?(name)
799
+ @bindings.key?(name)
800
+ end
701
801
  end
702
802
  end
@@ -20,38 +20,35 @@ module Playwright
20
20
  @electron ||= ::Playwright::ChannelOwners::Electron.from(@initializer['electron'])
21
21
  end
22
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
23
+ def selectors
24
+ @selectors ||= ::Playwright::ChannelOwners::Selectors.from(@initializer['selectors'])
49
25
  end
50
26
 
51
27
  def devices
52
28
  @devices ||= @initializer['deviceDescriptors'].map do |item|
53
- [item['name'], DeviceDescriptor.new(item['descriptor'])]
29
+ [item['name'], parse_device_descriptor(item['descriptor'])]
54
30
  end.to_h
55
31
  end
32
+
33
+ private def parse_device_descriptor(descriptor)
34
+ # This return value can be passed into Browser#new_context as it is.
35
+ # ex:
36
+ # ```
37
+ # iPhone = playwright.devices['iPhone 6']
38
+ # context = browser.new_context(**iPhone)
39
+ # page = context.new_page
40
+ #
41
+ # ```
42
+ {
43
+ userAgent: descriptor['userAgent'],
44
+ viewport: {
45
+ width: descriptor['viewport']['width'],
46
+ height: descriptor['viewport']['height'],
47
+ },
48
+ deviceScaleFactor: descriptor['deviceScaleFactor'],
49
+ isMobile: descriptor['isMobile'],
50
+ hasTouch: descriptor['hasTouch'],
51
+ }
52
+ end
56
53
  end
57
54
  end
@@ -29,7 +29,7 @@ module Playwright
29
29
  end
30
30
 
31
31
  def method
32
- @initialize['method']
32
+ @initializer['method']
33
33
  end
34
34
 
35
35
  def post_data
@@ -69,7 +69,7 @@ module Playwright
69
69
  end
70
70
 
71
71
  def frame
72
- @initializer['frame']
72
+ ChannelOwners::Frame.from(@initializer['frame'])
73
73
  end
74
74
 
75
75
  def navigation_request?
@@ -90,6 +90,30 @@ module Playwright
90
90
  @failure_text = failure_text
91
91
  end
92
92
 
93
+ private def update_timings(
94
+ start_time:,
95
+ domain_lookup_start:,
96
+ domain_lookup_end:,
97
+ connect_start:,
98
+ secure_connection_start:,
99
+ connect_end:,
100
+ request_start:,
101
+ response_start:)
102
+
103
+ @timing["startTime"] = start_time
104
+ @timing["domainLookupStart"] = domain_lookup_start
105
+ @timing["domainLookupEnd"] = domain_lookup_end
106
+ @timing["connectStart"] = connect_start
107
+ @timing["secureConnectionStart"] = secure_connection_start
108
+ @timing["connectEnd"] = connect_end
109
+ @timing["requestStart"] = request_start
110
+ @timing["responseStart"] = response_start
111
+ end
112
+
113
+ private def update_headers(headers)
114
+ @headers = parse_headers(headers)
115
+ end
116
+
93
117
  private def update_response_end_timing(response_end_timing)
94
118
  @timing[:responseEnd] = response_end_timing
95
119
  end
@@ -1,5 +1,65 @@
1
+ require 'base64'
2
+ require 'json'
3
+
1
4
  module Playwright
2
5
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_network.py
3
6
  define_channel_owner :Response do
7
+ def after_initialize
8
+ @request = ChannelOwners::Request.from(@initializer['request'])
9
+ timing = @initializer['timing']
10
+ @request.send(:update_timings,
11
+ start_time: timing["startTime"],
12
+ domain_lookup_start: timing["domainLookupStart"],
13
+ domain_lookup_end: timing["domainLookupEnd"],
14
+ connect_start: timing["connectStart"],
15
+ secure_connection_start: timing["secureConnectionStart"],
16
+ connect_end: timing["connectEnd"],
17
+ request_start: timing["requestStart"],
18
+ response_start: timing["responseStart"],
19
+ )
20
+ @request.send(:update_headers, @initializer['requestHeaders'])
21
+ end
22
+ attr_reader :request
23
+
24
+ def url
25
+ @initializer['url']
26
+ end
27
+
28
+ def ok
29
+ status == 0 || (200...300).include?(status)
30
+ end
31
+ alias_method :ok?, :ok
32
+
33
+ def status
34
+ @initializer['status']
35
+ end
36
+
37
+ def status_text
38
+ @initializer['statusText']
39
+ end
40
+
41
+ def headers
42
+ @initializer['headers'].map do |header|
43
+ [header['name'].downcase, header['value']]
44
+ end.to_h
45
+ end
46
+
47
+ def finished
48
+ @channel.send_message_to_server('finished')
49
+ end
50
+
51
+ def body
52
+ binary = @channel.send_message_to_server("body")
53
+ Base64.strict_decode64(binary)
54
+ end
55
+ alias_method :text, :body
56
+
57
+ def json
58
+ JSON.parse(text)
59
+ end
60
+
61
+ def frame
62
+ @request.frame
63
+ end
4
64
  end
5
65
  end