playwright-ruby-client 0.0.4 → 0.0.9

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 +120 -6
  3. data/docs/api_coverage.md +351 -0
  4. data/lib/playwright.rb +9 -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 +20 -27
  9. data/lib/playwright/channel_owners/browser_context.rb +51 -0
  10. data/lib/playwright/channel_owners/console_message.rb +0 -4
  11. data/lib/playwright/channel_owners/element_handle.rb +306 -0
  12. data/lib/playwright/channel_owners/frame.rb +473 -7
  13. data/lib/playwright/channel_owners/js_handle.rb +51 -0
  14. data/lib/playwright/channel_owners/page.rb +589 -4
  15. data/lib/playwright/channel_owners/request.rb +98 -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 +17 -1
  20. data/lib/playwright/http_headers.rb +20 -0
  21. data/lib/playwright/input_files.rb +42 -0
  22. data/lib/playwright/input_type.rb +19 -0
  23. data/lib/playwright/input_types/android_input.rb +19 -0
  24. data/lib/playwright/input_types/keyboard.rb +32 -0
  25. data/lib/playwright/input_types/mouse.rb +4 -0
  26. data/lib/playwright/input_types/touchscreen.rb +4 -0
  27. data/lib/playwright/javascript.rb +13 -0
  28. data/lib/playwright/javascript/expression.rb +67 -0
  29. data/lib/playwright/javascript/function.rb +67 -0
  30. data/lib/playwright/javascript/value_parser.rb +75 -0
  31. data/lib/playwright/javascript/value_serializer.rb +54 -0
  32. data/lib/playwright/playwright_api.rb +45 -25
  33. data/lib/playwright/select_option_values.rb +32 -0
  34. data/lib/playwright/timeout_settings.rb +19 -0
  35. data/lib/playwright/url_matcher.rb +19 -0
  36. data/lib/playwright/utils.rb +37 -0
  37. data/lib/playwright/version.rb +1 -1
  38. data/lib/playwright/wait_helper.rb +73 -0
  39. data/lib/playwright_api/accessibility.rb +46 -6
  40. data/lib/playwright_api/android.rb +33 -0
  41. data/lib/playwright_api/android_device.rb +78 -0
  42. data/lib/playwright_api/android_input.rb +25 -0
  43. data/lib/playwright_api/binding_call.rb +18 -0
  44. data/lib/playwright_api/browser.rb +93 -12
  45. data/lib/playwright_api/browser_context.rb +279 -28
  46. data/lib/playwright_api/browser_type.rb +68 -5
  47. data/lib/playwright_api/cdp_session.rb +23 -1
  48. data/lib/playwright_api/chromium_browser_context.rb +26 -0
  49. data/lib/playwright_api/console_message.rb +20 -7
  50. data/lib/playwright_api/dialog.rb +48 -2
  51. data/lib/playwright_api/download.rb +19 -4
  52. data/lib/playwright_api/element_handle.rb +278 -104
  53. data/lib/playwright_api/file_chooser.rb +20 -3
  54. data/lib/playwright_api/frame.rb +452 -147
  55. data/lib/playwright_api/js_handle.rb +78 -19
  56. data/lib/playwright_api/keyboard.rb +99 -9
  57. data/lib/playwright_api/mouse.rb +22 -0
  58. data/lib/playwright_api/page.rb +864 -222
  59. data/lib/playwright_api/playwright.rb +116 -14
  60. data/lib/playwright_api/request.rb +86 -24
  61. data/lib/playwright_api/response.rb +18 -7
  62. data/lib/playwright_api/route.rb +49 -0
  63. data/lib/playwright_api/selectors.rb +28 -2
  64. data/lib/playwright_api/video.rb +8 -0
  65. data/lib/playwright_api/web_socket.rb +0 -8
  66. data/lib/playwright_api/worker.rb +25 -13
  67. data/playwright.gemspec +3 -0
  68. metadata +66 -2
@@ -1,5 +1,103 @@
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 update_failure_text(failure_text)
90
+ @failure_text = failure_text
91
+ end
92
+
93
+ private def update_response_end_timing(response_end_timing)
94
+ @timing[:responseEnd] = response_end_timing
95
+ end
96
+
97
+ private def parse_headers(headers)
98
+ headers.map do |header|
99
+ [header['name'].downcase, header['value']]
100
+ end.to_h
101
+ end
4
102
  end
5
103
  end
@@ -2,7 +2,7 @@ require_relative './browser'
2
2
 
3
3
  module Playwright
4
4
  module ChannelOwners
5
- class WebkitBrowser < Browser
5
+ class WebKitBrowser < Browser
6
6
  end
7
7
  end
8
8
  end
@@ -55,10 +55,13 @@ module Playwright
55
55
  method: method,
56
56
  params: replace_channels_with_guids(params),
57
57
  }
58
- @transport.send_message(message)
59
- rescue Transport::AlreadyDisconnectedError => err
60
- @callbacks.delete(id)
61
- callback.reject(err)
58
+ begin
59
+ @transport.send_message(message)
60
+ rescue => err
61
+ @callbacks.delete(id)
62
+ callback.reject(err)
63
+ raise unless err.is_a?(Transport::AlreadyDisconnectedError)
64
+ end
62
65
  end
63
66
 
64
67
  callback
@@ -132,7 +135,7 @@ module Playwright
132
135
  unless object
133
136
  raise "Cannot find object to dispose: #{guid}"
134
137
  end
135
- object.dispose
138
+ object.send(:dispose!)
136
139
  return
137
140
  end
138
141
 
@@ -153,7 +156,7 @@ module Playwright
153
156
  end
154
157
 
155
158
  if payload.is_a?(Channel)
156
- { guid: payload.guid }
159
+ return { guid: payload.guid }
157
160
  end
158
161
 
159
162
  if payload.is_a?(Hash)
@@ -192,12 +195,6 @@ module Playwright
192
195
  end
193
196
  initializer = replace_guids_with_channels(initializer)
194
197
 
195
- params = [
196
- parent,
197
- type,
198
- guid,
199
- initializer,
200
- ]
201
198
  class_name = case type
202
199
  when 'Browser'
203
200
  case initializer['name']
@@ -215,7 +212,6 @@ module Playwright
215
212
  if browser_name == 'chromium'
216
213
  'ChromiumBrowserContext'
217
214
  else
218
- params << browser_name
219
215
  'BrowserContext'
220
216
  end
221
217
  else
@@ -224,7 +220,12 @@ module Playwright
224
220
 
225
221
  result =
226
222
  begin
227
- ChannelOwners.const_get(class_name).new(*params)
223
+ ChannelOwners.const_get(class_name).new(
224
+ parent,
225
+ type,
226
+ guid,
227
+ initializer,
228
+ )
228
229
  rescue NameError
229
230
  raise "Missing type #{type}"
230
231
  end
@@ -3,7 +3,7 @@ module Playwright
3
3
  # ref: https://github.com/microsoft/playwright-python/blob/0b4a980fed366c4c1dee9bfcdd72662d629fdc8d/playwright/_impl/_helper.py#L155
4
4
  def self.parse(error_payload)
5
5
  if error_payload['name'] == 'TimeoutError'
6
- Playwright::TimeoutError.new(
6
+ TimeoutError.new(
7
7
  message: error_payload['message'],
8
8
  stack: error_payload['stack'].split("\n"),
9
9
  )
@@ -17,15 +17,31 @@ module Playwright
17
17
  end
18
18
  end
19
19
 
20
+ module EventListenerInterface
21
+ def on(event, callback)
22
+ raise NotImplementedError.new('NOT IMPLEMENTED')
23
+ end
24
+
25
+ def off(event, callback)
26
+ raise NotImplementedError.new('NOT IMPLEMENTED')
27
+ end
28
+
29
+ def once(event, callback)
30
+ raise NotImplementedError.new('NOT IMPLEMENTED')
31
+ end
32
+ end
20
33
 
21
34
  # A subset of Events/EventEmitter in Node.js
22
35
  module EventEmitter
23
36
  # @param event [String]
37
+ # @returns [Boolean]
24
38
  def emit(event, *args)
39
+ handled = false
25
40
  (@__event_emitter ||= {})[event.to_s]&.each do |callback|
26
41
  callback.call(*args)
42
+ handled = true
27
43
  end
28
- self
44
+ handled
29
45
  end
30
46
 
31
47
  # @param event [String]
@@ -0,0 +1,20 @@
1
+ module Playwright
2
+ class HttpHeaders
3
+ # @param headers [Hash]
4
+ def initialize(headers)
5
+ @headers = headers
6
+ end
7
+
8
+ def as_serialized
9
+ @headers.map do |key, value|
10
+ { name: key, value: value }
11
+ end
12
+ end
13
+
14
+ def self.parse_serialized(serialized_headers)
15
+ new(serialized_headers.map do |header|
16
+ [header['name'].downcase, header['value']]
17
+ end.to_h)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ require 'base64'
2
+ require 'mime/types'
3
+
4
+ module Playwright
5
+ class InputFiles
6
+ def initialize(files)
7
+ @params = convert(files)
8
+ end
9
+
10
+ def as_params
11
+ @params
12
+ end
13
+
14
+ private def convert(files)
15
+ return convert([files]) unless files.is_a?(Array)
16
+
17
+ files.map do |file|
18
+ case file
19
+ when String
20
+ {
21
+ name: File.basename(file),
22
+ mimeType: mime_type_for(file),
23
+ buffer: Base64.strict_encode64(File.read(file)),
24
+ }
25
+ when File
26
+ {
27
+ name: File.basename(file.path),
28
+ mimeType: mime_type_for(file.path),
29
+ buffer: Base64.strict_encode64(file.read),
30
+ }
31
+ else
32
+ raise ArgumentError.new('file must be a String or File.')
33
+ end
34
+ end
35
+ end
36
+
37
+ private def mime_type_for(filepath)
38
+ mime_types = MIME::Types.type_for(filepath)
39
+ mime_types.first || 'application/octet-stream'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module Playwright
2
+ class InputType
3
+ def initialize(channel)
4
+ @channel = channel
5
+ end
6
+ end
7
+
8
+ # namespace declaration
9
+ module InputTypes ; end
10
+
11
+ def self.define_input_type(class_name, &block)
12
+ klass = Class.new(InputType)
13
+ klass.class_eval(&block) if block
14
+ InputTypes.const_set(class_name, klass)
15
+ end
16
+ end
17
+
18
+ # load subclasses
19
+ Dir[File.join(__dir__, 'input_types', '*.rb')].each { |f| require f }
@@ -0,0 +1,19 @@
1
+ module Playwright
2
+ define_input_type :AndroidInput do
3
+ def type(text)
4
+ @channel.send_message_to_server('inputType', text: text)
5
+ end
6
+
7
+ def press(key)
8
+ @channel.send_message_to_server('inputPress', key: key)
9
+ end
10
+
11
+ def tap_point(point)
12
+ @channel.send_message_to_server('inputTap', point: point)
13
+ end
14
+
15
+ def drag(from, to, steps)
16
+ @channel.send_message_to_server('inputDrag', from: from, to: to, steps: steps)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Playwright
2
+ define_input_type :Keyboard do
3
+ def down(key)
4
+ @channel.send_message_to_server('keyboardDown', key: key)
5
+ nil
6
+ end
7
+
8
+ def up(key)
9
+ @channel.send_message_to_server('keyboardDown', key: key)
10
+ end
11
+
12
+ def insert_text(text)
13
+ @channel.send_message_to_server('keyboardInsertText', text: text)
14
+ end
15
+
16
+ def type(text, delay: nil)
17
+ params = {
18
+ text: text,
19
+ delay: delay,
20
+ }.compact
21
+ @channel.send_message_to_server('keyboardType', params)
22
+ end
23
+
24
+ def press(key, delay: nil)
25
+ params = {
26
+ key: key,
27
+ delay: delay,
28
+ }.compact
29
+ @channel.send_message_to_server('keyboardPress', params)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module Playwright
2
+ define_input_type :Mouse do
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Playwright
2
+ define_input_type :Touchscreen do
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ require_relative './javascript/expression'
2
+ require_relative './javascript/function'
3
+ require_relative './javascript/value_parser'
4
+ require_relative './javascript/value_serializer'
5
+
6
+ module Playwright
7
+ module JavaScript
8
+ # Detect if str is likely to be a function
9
+ module_function def function?(str)
10
+ ['async', 'function'].any? { |key| str.strip.start_with?(key) } || str.include?('=>')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,67 @@
1
+ module Playwright
2
+ module JavaScript
3
+ class Expression
4
+ def initialize(expression)
5
+ @expression = expression
6
+ @serialized_arg = ValueSerializer.new(nil).serialize
7
+ end
8
+
9
+ def evaluate(channel)
10
+ value = channel.send_message_to_server(
11
+ 'evaluateExpression',
12
+ expression: @expression,
13
+ isFunction: false,
14
+ arg: @serialized_arg,
15
+ )
16
+ ValueParser.new(value).parse
17
+ end
18
+
19
+ def evaluate_handle(channel)
20
+ resp = channel.send_message_to_server(
21
+ 'evaluateExpressionHandle',
22
+ expression: @expression,
23
+ isFunction: false,
24
+ arg: @serialized_arg,
25
+ )
26
+ ::Playwright::ChannelOwner.from(resp)
27
+ end
28
+
29
+ def eval_on_selector(channel, selector)
30
+ value = channel.send_message_to_server(
31
+ 'evalOnSelector',
32
+ selector: selector,
33
+ expression: @expression,
34
+ isFunction: false,
35
+ arg: @serialized_arg,
36
+ )
37
+ ValueParser.new(value).parse
38
+ end
39
+
40
+ def eval_on_selector_all(channel, selector)
41
+ value = channel.send_message_to_server(
42
+ 'evalOnSelectorAll',
43
+ selector: selector,
44
+ expression: @expression,
45
+ isFunction: false,
46
+ arg: @serialized_arg,
47
+ )
48
+ ValueParser.new(value).parse
49
+ end
50
+
51
+ def wait_for_function(channel, polling:, timeout:)
52
+ params = {
53
+ expression: @expression,
54
+ isFunction: false,
55
+ arg: @serialized_arg,
56
+ polling: polling,
57
+ timeout: timeout,
58
+ }.compact
59
+ if polling.is_a?(Numeric)
60
+ params[:pollingInterval] = polling
61
+ end
62
+ resp = channel.send_message_to_server('waitForFunction', params)
63
+ ChannelOwners::JSHandle.from(resp)
64
+ end
65
+ end
66
+ end
67
+ end