playwright-ruby-client 0.0.8 → 1.58.1.alpha1

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 (209) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +4 -0
  3. data/CLAUDE/api_generation.md +28 -0
  4. data/CLAUDE/ci_expectations.md +23 -0
  5. data/CLAUDE/gem_release_flow.md +39 -0
  6. data/CLAUDE/past_upgrade_pr_patterns.md +42 -0
  7. data/CLAUDE/playwright_upgrade_workflow.md +35 -0
  8. data/CLAUDE/rspec_debugging.md +30 -0
  9. data/CLAUDE/unimplemented_examples.md +18 -0
  10. data/CLAUDE.md +32 -0
  11. data/CONTRIBUTING.md +5 -0
  12. data/README.md +60 -16
  13. data/documentation/README.md +33 -0
  14. data/documentation/babel.config.js +3 -0
  15. data/documentation/docs/api/api_request.md +7 -0
  16. data/documentation/docs/api/api_request_context.md +298 -0
  17. data/documentation/docs/api/api_response.md +114 -0
  18. data/documentation/docs/api/browser.md +237 -0
  19. data/documentation/docs/api/browser_context.md +503 -0
  20. data/documentation/docs/api/browser_type.md +184 -0
  21. data/documentation/docs/api/cdp_session.md +44 -0
  22. data/documentation/docs/api/clock.md +154 -0
  23. data/documentation/docs/api/console_message.md +85 -0
  24. data/documentation/docs/api/dialog.md +84 -0
  25. data/documentation/docs/api/download.md +111 -0
  26. data/documentation/docs/api/element_handle.md +694 -0
  27. data/documentation/docs/api/experimental/_category_.yml +3 -0
  28. data/documentation/docs/api/experimental/android.md +42 -0
  29. data/documentation/docs/api/experimental/android_device.md +109 -0
  30. data/documentation/docs/api/experimental/android_input.md +43 -0
  31. data/documentation/docs/api/experimental/android_socket.md +7 -0
  32. data/documentation/docs/api/experimental/android_web_view.md +7 -0
  33. data/documentation/docs/api/file_chooser.md +53 -0
  34. data/documentation/docs/api/frame.md +1218 -0
  35. data/documentation/docs/api/frame_locator.md +348 -0
  36. data/documentation/docs/api/js_handle.md +121 -0
  37. data/documentation/docs/api/keyboard.md +170 -0
  38. data/documentation/docs/api/locator.md +1495 -0
  39. data/documentation/docs/api/locator_assertions.md +827 -0
  40. data/documentation/docs/api/mouse.md +86 -0
  41. data/documentation/docs/api/page.md +1946 -0
  42. data/documentation/docs/api/page_assertions.md +65 -0
  43. data/documentation/docs/api/playwright.md +66 -0
  44. data/documentation/docs/api/request.md +255 -0
  45. data/documentation/docs/api/response.md +176 -0
  46. data/documentation/docs/api/route.md +205 -0
  47. data/documentation/docs/api/selectors.md +63 -0
  48. data/documentation/docs/api/touchscreen.md +22 -0
  49. data/documentation/docs/api/tracing.md +129 -0
  50. data/documentation/docs/api/web_socket.md +51 -0
  51. data/documentation/docs/api/worker.md +83 -0
  52. data/documentation/docs/article/api_coverage.mdx +11 -0
  53. data/documentation/docs/article/getting_started.md +161 -0
  54. data/documentation/docs/article/guides/_category_.yml +3 -0
  55. data/documentation/docs/article/guides/download_playwright_driver.md +55 -0
  56. data/documentation/docs/article/guides/inspector.md +31 -0
  57. data/documentation/docs/article/guides/launch_browser.md +121 -0
  58. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +112 -0
  59. data/documentation/docs/article/guides/rails_integration.md +278 -0
  60. data/documentation/docs/article/guides/rails_integration_with_null_driver.md +145 -0
  61. data/documentation/docs/article/guides/recording_video.md +79 -0
  62. data/documentation/docs/article/guides/rspec_integration.md +59 -0
  63. data/documentation/docs/article/guides/semi_automation.md +71 -0
  64. data/documentation/docs/article/guides/use_storage_state.md +78 -0
  65. data/documentation/docs/include/api_coverage.md +671 -0
  66. data/documentation/docusaurus.config.js +114 -0
  67. data/documentation/package.json +39 -0
  68. data/documentation/sidebars.js +15 -0
  69. data/documentation/src/components/HomepageFeatures.js +61 -0
  70. data/documentation/src/components/HomepageFeatures.module.css +13 -0
  71. data/documentation/src/css/custom.css +44 -0
  72. data/documentation/src/pages/index.js +49 -0
  73. data/documentation/src/pages/index.module.css +41 -0
  74. data/documentation/src/pages/markdown-page.md +7 -0
  75. data/documentation/static/.nojekyll +0 -0
  76. data/documentation/static/img/playwright-logo.svg +9 -0
  77. data/documentation/static/img/playwright-ruby-client.png +0 -0
  78. data/documentation/static/img/undraw_dropdown_menu.svg +1 -0
  79. data/documentation/static/img/undraw_web_development.svg +1 -0
  80. data/documentation/static/img/undraw_windows.svg +1 -0
  81. data/documentation/yarn.lock +9005 -0
  82. data/lib/playwright/{input_types/android_input.rb → android_input_impl.rb} +5 -1
  83. data/lib/playwright/api_implementation.rb +18 -0
  84. data/lib/playwright/api_response_impl.rb +77 -0
  85. data/lib/playwright/channel.rb +62 -1
  86. data/lib/playwright/channel_owner.rb +70 -7
  87. data/lib/playwright/channel_owners/android.rb +16 -3
  88. data/lib/playwright/channel_owners/android_device.rb +22 -66
  89. data/lib/playwright/channel_owners/api_request_context.rb +247 -0
  90. data/lib/playwright/channel_owners/artifact.rb +40 -0
  91. data/lib/playwright/channel_owners/binding_call.rb +70 -0
  92. data/lib/playwright/channel_owners/browser.rb +114 -22
  93. data/lib/playwright/channel_owners/browser_context.rb +589 -15
  94. data/lib/playwright/channel_owners/browser_type.rb +90 -1
  95. data/lib/playwright/channel_owners/cdp_session.rb +19 -0
  96. data/lib/playwright/channel_owners/dialog.rb +32 -0
  97. data/lib/playwright/channel_owners/element_handle.rb +107 -43
  98. data/lib/playwright/channel_owners/fetch_request.rb +8 -0
  99. data/lib/playwright/channel_owners/frame.rb +334 -104
  100. data/lib/playwright/channel_owners/js_handle.rb +9 -13
  101. data/lib/playwright/channel_owners/local_utils.rb +82 -0
  102. data/lib/playwright/channel_owners/page.rb +778 -95
  103. data/lib/playwright/channel_owners/playwright.rb +25 -30
  104. data/lib/playwright/channel_owners/request.rb +120 -18
  105. data/lib/playwright/channel_owners/response.rb +113 -0
  106. data/lib/playwright/channel_owners/route.rb +181 -0
  107. data/lib/playwright/channel_owners/stream.rb +30 -0
  108. data/lib/playwright/channel_owners/tracing.rb +117 -0
  109. data/lib/playwright/channel_owners/web_socket.rb +96 -0
  110. data/lib/playwright/channel_owners/worker.rb +46 -0
  111. data/lib/playwright/channel_owners/writable_stream.rb +14 -0
  112. data/lib/playwright/clock_impl.rb +67 -0
  113. data/lib/playwright/connection.rb +111 -63
  114. data/lib/playwright/console_message_impl.rb +29 -0
  115. data/lib/playwright/download_impl.rb +32 -0
  116. data/lib/playwright/errors.rb +42 -5
  117. data/lib/playwright/event_emitter.rb +17 -3
  118. data/lib/playwright/event_emitter_proxy.rb +49 -0
  119. data/lib/playwright/events.rb +10 -5
  120. data/lib/playwright/file_chooser_impl.rb +24 -0
  121. data/lib/playwright/frame_locator_impl.rb +66 -0
  122. data/lib/playwright/har_router.rb +89 -0
  123. data/lib/playwright/http_headers.rb +14 -0
  124. data/lib/playwright/input_files.rb +102 -15
  125. data/lib/playwright/javascript/expression.rb +7 -11
  126. data/lib/playwright/javascript/regex.rb +23 -0
  127. data/lib/playwright/javascript/source_url.rb +16 -0
  128. data/lib/playwright/javascript/value_parser.rb +108 -19
  129. data/lib/playwright/javascript/value_serializer.rb +47 -8
  130. data/lib/playwright/javascript/visitor_info.rb +26 -0
  131. data/lib/playwright/javascript.rb +2 -10
  132. data/lib/playwright/{input_types/keyboard.rb → keyboard_impl.rb} +6 -2
  133. data/lib/playwright/locator_assertions_impl.rb +571 -0
  134. data/lib/playwright/locator_impl.rb +544 -0
  135. data/lib/playwright/locator_utils.rb +136 -0
  136. data/lib/playwright/mouse_impl.rb +57 -0
  137. data/lib/playwright/page_assertions_impl.rb +154 -0
  138. data/lib/playwright/playwright_api.rb +102 -30
  139. data/lib/playwright/raw_headers.rb +61 -0
  140. data/lib/playwright/route_handler.rb +78 -0
  141. data/lib/playwright/select_option_values.rb +34 -13
  142. data/lib/playwright/selectors_impl.rb +45 -0
  143. data/lib/playwright/test.rb +102 -0
  144. data/lib/playwright/timeout_settings.rb +9 -4
  145. data/lib/playwright/touchscreen_impl.rb +14 -0
  146. data/lib/playwright/transport.rb +61 -10
  147. data/lib/playwright/url_matcher.rb +24 -2
  148. data/lib/playwright/utils.rb +48 -13
  149. data/lib/playwright/version.rb +2 -1
  150. data/lib/playwright/video.rb +54 -0
  151. data/lib/playwright/waiter.rb +166 -0
  152. data/lib/playwright/web_socket_client.rb +167 -0
  153. data/lib/playwright/web_socket_transport.rb +116 -0
  154. data/lib/playwright.rb +188 -11
  155. data/lib/playwright_api/android.rb +46 -11
  156. data/lib/playwright_api/android_device.rb +182 -31
  157. data/lib/playwright_api/android_input.rb +22 -13
  158. data/lib/playwright_api/android_socket.rb +18 -0
  159. data/lib/playwright_api/android_web_view.rb +24 -0
  160. data/lib/playwright_api/api_request.rb +26 -0
  161. data/lib/playwright_api/api_request_context.rb +311 -0
  162. data/lib/playwright_api/api_response.rb +92 -0
  163. data/lib/playwright_api/browser.rb +116 -103
  164. data/lib/playwright_api/browser_context.rb +290 -389
  165. data/lib/playwright_api/browser_type.rb +96 -118
  166. data/lib/playwright_api/cdp_session.rb +36 -39
  167. data/lib/playwright_api/clock.rb +121 -0
  168. data/lib/playwright_api/console_message.rb +35 -19
  169. data/lib/playwright_api/dialog.rb +53 -50
  170. data/lib/playwright_api/download.rb +49 -43
  171. data/lib/playwright_api/element_handle.rb +354 -402
  172. data/lib/playwright_api/file_chooser.rb +15 -18
  173. data/lib/playwright_api/frame.rb +703 -603
  174. data/lib/playwright_api/frame_locator.rb +285 -0
  175. data/lib/playwright_api/js_handle.rb +50 -76
  176. data/lib/playwright_api/keyboard.rb +67 -146
  177. data/lib/playwright_api/locator.rb +1304 -0
  178. data/lib/playwright_api/locator_assertions.rb +704 -0
  179. data/lib/playwright_api/mouse.rb +23 -29
  180. data/lib/playwright_api/page.rb +1196 -1176
  181. data/lib/playwright_api/page_assertions.rb +60 -0
  182. data/lib/playwright_api/playwright.rb +54 -122
  183. data/lib/playwright_api/request.rb +112 -74
  184. data/lib/playwright_api/response.rb +92 -20
  185. data/lib/playwright_api/route.rb +152 -62
  186. data/lib/playwright_api/selectors.rb +47 -61
  187. data/lib/playwright_api/touchscreen.rb +8 -2
  188. data/lib/playwright_api/tracing.rb +128 -0
  189. data/lib/playwright_api/web_socket.rb +43 -5
  190. data/lib/playwright_api/worker.rb +74 -34
  191. data/playwright.gemspec +14 -9
  192. data/sig/playwright.rbs +658 -0
  193. metadata +216 -50
  194. data/docs/api_coverage.md +0 -354
  195. data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
  196. data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
  197. data/lib/playwright/channel_owners/console_message.rb +0 -21
  198. data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
  199. data/lib/playwright/channel_owners/selectors.rb +0 -4
  200. data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
  201. data/lib/playwright/input_type.rb +0 -19
  202. data/lib/playwright/input_types/mouse.rb +0 -4
  203. data/lib/playwright/input_types/touchscreen.rb +0 -4
  204. data/lib/playwright/javascript/function.rb +0 -67
  205. data/lib/playwright/wait_helper.rb +0 -73
  206. data/lib/playwright_api/accessibility.rb +0 -93
  207. data/lib/playwright_api/binding_call.rb +0 -23
  208. data/lib/playwright_api/chromium_browser_context.rb +0 -57
  209. data/lib/playwright_api/video.rb +0 -24
@@ -1,5 +1,9 @@
1
1
  module Playwright
2
- define_input_type :AndroidInput do
2
+ define_api_implementation :AndroidInputImpl do
3
+ def initialize(channel)
4
+ @channel = channel
5
+ end
6
+
3
7
  def type(text)
4
8
  @channel.send_message_to_server('inputType', text: text)
5
9
  end
@@ -0,0 +1,18 @@
1
+ module Playwright
2
+ # Each Impl class include this module.
3
+ # Used for detecting whether the object is a XXXXImpl or not.
4
+ module ApiImplementation ; end
5
+
6
+ def self.define_api_implementation(class_name, &block)
7
+ klass = Class.new
8
+ klass.include(ApiImplementation)
9
+ klass.class_eval(&block) if block
10
+ if ::Playwright.const_defined?(class_name)
11
+ raise ArgumentError.new("Playwright::#{class_name} already exist. Choose another class name.")
12
+ end
13
+ ::Playwright.const_set(class_name, klass)
14
+ end
15
+ end
16
+
17
+ # load subclasses
18
+ Dir[File.join(__dir__, '*_impl.rb')].each { |f| require f }
@@ -0,0 +1,77 @@
1
+ module Playwright
2
+ define_api_implementation :APIResponseImpl do
3
+ include Utils::Errors::TargetClosedErrorMethods
4
+
5
+ # @params context [APIRequestContext]
6
+ # @params initializer [Hash]
7
+ def initialize(context, initializer)
8
+ @request = context
9
+ @initializer = initializer
10
+ @headers = RawHeaders.new(initializer['headers'])
11
+ end
12
+
13
+ def to_s
14
+ "#<APIResponse url=#{url} status=#{status} status_text=#{status_text}>"
15
+ end
16
+
17
+ def url
18
+ @initializer['url']
19
+ end
20
+
21
+ def ok
22
+ (200...300).include?(status)
23
+ end
24
+ alias_method :ok?, :ok
25
+
26
+ def status
27
+ @initializer['status']
28
+ end
29
+
30
+ def status_text
31
+ @initializer['statusText']
32
+ end
33
+
34
+ def headers
35
+ @headers.headers
36
+ end
37
+
38
+ def headers_array
39
+ @headers.headers_array
40
+ end
41
+
42
+ class AlreadyDisposedError < StandardError
43
+ def initialize
44
+ super('Response has been disposed')
45
+ end
46
+ end
47
+
48
+ def body
49
+ binary = @request.channel.send_message_to_server("fetchResponseBody", fetchUid: fetch_uid)
50
+ raise AlreadyDisposedError.new unless binary
51
+ Base64.strict_decode64(binary)
52
+ rescue => err
53
+ if target_closed_error?(err)
54
+ raise AlreadyDisposedError.new
55
+ else
56
+ raise
57
+ end
58
+ end
59
+ alias_method :text, :body
60
+
61
+ def json
62
+ JSON.parse(text)
63
+ end
64
+
65
+ def dispose
66
+ @request.channel.send_message_to_server("disposeAPIResponse", fetchUid: fetch_uid)
67
+ end
68
+
69
+ private def _request
70
+ @request
71
+ end
72
+
73
+ private def fetch_uid
74
+ @initializer['fetchUid']
75
+ end
76
+ end
77
+ end
@@ -15,8 +15,9 @@ module Playwright
15
15
 
16
16
  # @param method [String]
17
17
  # @param params [Hash]
18
+ # @return [Playwright::ChannelOwner|nil]
18
19
  def send_message_to_server(method, params = {})
19
- result = @connection.send_message_to_server(@guid, method, params)
20
+ result = send_message_to_server_result(method, params)
20
21
  if result.is_a?(Hash)
21
22
  _type, channel_owner = result.first
22
23
  channel_owner
@@ -24,5 +25,65 @@ module Playwright
24
25
  nil
25
26
  end
26
27
  end
28
+
29
+ # @param method [String]
30
+ # @param params [Hash]
31
+ # @return [Hash]
32
+ def send_message_to_server_result(title = nil, method, params)
33
+ check_not_collected
34
+ with_logging(title) do |metadata|
35
+ @connection.send_message_to_server(@guid, method, params, metadata: metadata)
36
+ end
37
+ end
38
+
39
+ # @param method [String]
40
+ # @param params [Hash]
41
+ # @returns [Concurrent::Promises::Future]
42
+ def async_send_message_to_server(method, params = {})
43
+ check_not_collected
44
+ with_logging do |metadata|
45
+ @connection.async_send_message_to_server(@guid, method, params, metadata: metadata)
46
+ end
47
+ end
48
+
49
+ private def with_logging(title = nil, &block)
50
+ locations = caller_locations
51
+ first_api_call_location_idx = locations.index { |loc| loc.absolute_path&.include?('playwright_api') }
52
+ unless first_api_call_location_idx
53
+ return block.call(nil)
54
+ end
55
+
56
+ locations = locations.slice(first_api_call_location_idx..-1)
57
+
58
+ api_call_location = locations.shift
59
+
60
+ api_class = File.basename(api_call_location.absolute_path, '.rb')
61
+ api_method = api_call_location.label
62
+ api_name = "#{api_class}##{api_method}"
63
+
64
+ stacks = locations
65
+
66
+ metadata = build_metadata_payload_from(api_name, stacks)
67
+ if title
68
+ metadata[:title] = title
69
+ end
70
+ block.call(metadata)
71
+ end
72
+
73
+ private def build_metadata_payload_from(api_name, stacks)
74
+ {
75
+ wallTime: (Time.now.to_f * 1000).to_i,
76
+ apiName: api_name,
77
+ stack: stacks.map do |loc|
78
+ { file: loc.absolute_path || '', line: loc.lineno, function: loc.label }
79
+ end,
80
+ }
81
+ end
82
+
83
+ private def check_not_collected
84
+ if @object.was_collected?
85
+ raise "The object has been collected to prevent unbounded heap growth."
86
+ end
87
+ end
27
88
  end
28
89
  end
@@ -10,6 +10,9 @@ module Playwright
10
10
  channel&.object
11
11
  end
12
12
 
13
+ # hidden field for caching API instance.
14
+ attr_accessor :_api
15
+
13
16
  # @param parent [Playwright::ChannelOwner|Playwright::Connection]
14
17
  # @param type [String]
15
18
  # @param guid [String]
@@ -33,23 +36,75 @@ module Playwright
33
36
  @type = type
34
37
  @guid = guid
35
38
  @initializer = initializer
39
+ @event_to_subscription_mapping = {}
36
40
 
37
41
  after_initialize
38
42
  end
39
43
 
40
44
  attr_reader :channel
41
45
 
46
+ private def adopt!(child)
47
+ unless child.is_a?(ChannelOwner)
48
+ raise ArgumentError.new("child must be a ChannelOwner: #{child.inspect}")
49
+ end
50
+ child.send(:update_parent, self)
51
+ end
52
+
53
+ def was_collected?
54
+ @was_collected
55
+ end
56
+
42
57
  # used only from Connection. Not intended for public use. So keep private.
43
- private def dispose!
58
+ private def dispose!(reason: nil)
44
59
  # Clean up from parent and connection.
45
- @parent&.send(:delete_object_from_child, @guid)
46
60
  @connection.send(:delete_object_from_channel_owner, @guid)
61
+ @was_collected = reason == 'gc'
47
62
 
48
63
  # Dispose all children.
49
- @objects.each_value { |object| object.send(:dispose!) }
64
+ @objects.each_value { |object| object.send(:dispose!, reason: reason) }
50
65
  @objects.clear
51
66
  end
52
67
 
68
+ private def set_event_to_subscription_mapping(event_to_subscription_mapping)
69
+ @event_to_subscription_mapping = event_to_subscription_mapping
70
+ end
71
+
72
+ private def update_subscription(event, enabled)
73
+ protocol_event = @event_to_subscription_mapping[event]
74
+ if protocol_event
75
+ payload = {
76
+ event: protocol_event,
77
+ enabled: enabled,
78
+ }
79
+ @channel.async_send_message_to_server('updateSubscription', payload)
80
+ end
81
+ end
82
+
83
+ # @override
84
+ def on(event, callback)
85
+ if listener_count(event) == 0
86
+ update_subscription(event, true)
87
+ end
88
+ super
89
+ end
90
+
91
+ # @override
92
+ def once(event, callback)
93
+ if listener_count(event) == 0
94
+ update_subscription(event, true)
95
+ end
96
+ super
97
+ end
98
+
99
+ # @override
100
+ def off(event, callback)
101
+ super
102
+ if listener_count(event) == 0
103
+ update_subscription(event, false)
104
+ end
105
+ end
106
+
107
+
53
108
  # Suppress long long inspect log and avoid RSpec from hanging up...
54
109
  def inspect
55
110
  to_s
@@ -59,18 +114,26 @@ module Playwright
59
114
  "#<#{@guid}>"
60
115
  end
61
116
 
62
- private
117
+ private def after_initialize
118
+ end
63
119
 
64
- def after_initialize
120
+ private def update_parent(new_parent)
121
+ @parent.send(:delete_object_from_child, @guid)
122
+ new_parent.send(:update_object_from_child, @guid, self)
123
+ @parent = new_parent
65
124
  end
66
125
 
67
- def update_object_from_child(guid, child)
126
+ private def update_object_from_child(guid, child)
68
127
  @objects[guid] = child
69
128
  end
70
129
 
71
- def delete_object_from_child(guid)
130
+ private def delete_object_from_child(guid)
72
131
  @objects.delete(guid)
73
132
  end
133
+
134
+ private def same_connection?(other)
135
+ @connection == other.instance_variable_get(:@connection)
136
+ end
74
137
  end
75
138
 
76
139
  class RootChannelOwner < ChannelOwner
@@ -1,11 +1,24 @@
1
1
  module Playwright
2
2
  define_channel_owner :Android do
3
- def after_initialize
3
+ private def after_initialize
4
4
  @timeout_settings = TimeoutSettings.new
5
5
  end
6
6
 
7
- def devices
8
- resp = @channel.send_message_to_server('devices')
7
+ def set_default_navigation_timeout(timeout)
8
+ @timeout_settings.default_navigation_timeout = timeout
9
+ end
10
+
11
+ def set_default_timeout(timeout)
12
+ @timeout_settings.default_timeout = timeout
13
+ end
14
+
15
+ private def _timeout_settings
16
+ @timeout_settings
17
+ end
18
+
19
+ def devices(host: nil, omitDriverInstall: nil, port: nil)
20
+ params = { host: host, port: port, omitDriverInstall: omitDriverInstall }.compact
21
+ resp = @channel.send_message_to_server('devices', params)
9
22
  resp.map { |device| ChannelOwners::AndroidDevice.from(device) }
10
23
  end
11
24
  end
@@ -2,8 +2,14 @@ module Playwright
2
2
  define_channel_owner :AndroidDevice do
3
3
  include Utils::PrepareBrowserContextOptions
4
4
 
5
- def after_initialize
6
- @input = InputTypes::AndroidInput.new(@channel)
5
+ private def after_initialize
6
+ @input = AndroidInputImpl.new(@channel)
7
+ @should_close_connection_on_close = false
8
+ @timeout_settings = @parent.send(:_timeout_settings)
9
+ end
10
+
11
+ def should_close_connection_on_close!
12
+ @should_close_connection_on_close = true
7
13
  end
8
14
 
9
15
  attr_reader :input
@@ -41,9 +47,9 @@ module Playwright
41
47
  enabled: selector[:enabled],
42
48
  focusable: selector[:focusable],
43
49
  focused: selector[:focused],
44
- hasChild: selector[:hasChild] ? { selector: to_selector_channel(selector[:hasChild][:selector]) } : nil,
50
+ hasChild: selector[:hasChild] ? { androidSelector: to_selector_channel(selector[:hasChild][:selector]) } : nil,
45
51
  hasDescendant: selector[:hasDescendant] ? {
46
- selector: to_selector_channel(selector[:hasDescendant][:selector]),
52
+ androidSelector: to_selector_channel(selector[:hasDescendant][:selector]),
47
53
  maxDepth: selector[:hasDescendant][:maxDepth],
48
54
  } : nil,
49
55
  longClickable: selector[:longClickable],
@@ -54,19 +60,15 @@ module Playwright
54
60
 
55
61
  def tap_on(selector, duration: nil, timeout: nil)
56
62
  params = {
57
- selector: to_selector_channel(selector),
63
+ androidSelector: to_selector_channel(selector),
58
64
  duration: duration,
59
- timeout: timeout,
65
+ timeout: @timeout_settings.timeout(timeout),
60
66
  }.compact
61
67
  @channel.send_message_to_server('tap', params)
62
68
  end
63
69
 
64
70
  def info(selector)
65
- @channel.send_message_to_server('info', selector: to_selector_channel(selector))
66
- end
67
-
68
- def tree
69
- @channel.send_message_to_server('tree')
71
+ @channel.send_message_to_server('info', androidSelector: to_selector_channel(selector))
70
72
  end
71
73
 
72
74
  def screenshot(path: nil)
@@ -81,8 +83,12 @@ module Playwright
81
83
  end
82
84
 
83
85
  def close
84
- @channel.send_message_to_server('close')
85
86
  emit(Events::AndroidDevice::Close)
87
+ if @should_close_connection_on_close
88
+ @connection.stop
89
+ else
90
+ @channel.send_message_to_server('close')
91
+ end
86
92
  end
87
93
 
88
94
  def shell(command)
@@ -90,60 +96,10 @@ module Playwright
90
96
  Base64.strict_decode64(resp)
91
97
  end
92
98
 
93
- def launch_browser(
94
- pkg: nil,
95
- acceptDownloads: nil,
96
- bypassCSP: nil,
97
- colorScheme: nil,
98
- deviceScaleFactor: nil,
99
- extraHTTPHeaders: nil,
100
- geolocation: nil,
101
- hasTouch: nil,
102
- httpCredentials: nil,
103
- ignoreHTTPSErrors: nil,
104
- isMobile: nil,
105
- javaScriptEnabled: nil,
106
- locale: nil,
107
- logger: nil,
108
- offline: nil,
109
- permissions: nil,
110
- proxy: nil,
111
- recordHar: nil,
112
- recordVideo: nil,
113
- storageState: nil,
114
- timezoneId: nil,
115
- userAgent: nil,
116
- videoSize: nil,
117
- videosPath: nil,
118
- viewport: nil,
119
- &block)
120
- params = {
121
- pkg: pkg,
122
- acceptDownloads: acceptDownloads,
123
- bypassCSP: bypassCSP,
124
- colorScheme: colorScheme,
125
- deviceScaleFactor: deviceScaleFactor,
126
- extraHTTPHeaders: extraHTTPHeaders,
127
- geolocation: geolocation,
128
- hasTouch: hasTouch,
129
- httpCredentials: httpCredentials,
130
- ignoreHTTPSErrors: ignoreHTTPSErrors,
131
- isMobile: isMobile,
132
- javaScriptEnabled: javaScriptEnabled,
133
- locale: locale,
134
- logger: logger,
135
- offline: offline,
136
- permissions: permissions,
137
- proxy: proxy,
138
- recordHar: recordHar,
139
- recordVideo: recordVideo,
140
- storageState: storageState,
141
- timezoneId: timezoneId,
142
- userAgent: userAgent,
143
- videoSize: videoSize,
144
- videosPath: videosPath,
145
- viewport: viewport,
146
- }.compact
99
+ def launch_browser(pkg: nil, **options, &block)
100
+ params = options.dup
101
+ params[:pkg] = pkg
102
+ params.compact!
147
103
  prepare_browser_context_options(params)
148
104
 
149
105
  resp = @channel.send_message_to_server('launchBrowser', params)
@@ -0,0 +1,247 @@
1
+ require 'base64'
2
+ require 'cgi/escape'
3
+
4
+ module Playwright
5
+ define_channel_owner :APIRequestContext do
6
+ private def after_initialize
7
+ @tracing = ChannelOwners::Tracing.from(@initializer['tracing'])
8
+ @timeout_settings = TimeoutSettings.new
9
+ end
10
+
11
+ private def _update_timeout_settings(timeout_settings)
12
+ @timeout_settings = timeout_settings
13
+ end
14
+
15
+ def dispose(reason: nil)
16
+ @close_reason = reason
17
+ @channel.send_message_to_server('dispose')
18
+ end
19
+
20
+ def delete(url, **options)
21
+ fetch_options = options.merge(method: 'DELETE')
22
+ fetch(url, **fetch_options)
23
+ end
24
+
25
+ def head(url, **options)
26
+ fetch_options = options.merge(method: 'HEAD')
27
+ fetch(url, **fetch_options)
28
+ end
29
+
30
+ def get(url, **options)
31
+ fetch_options = options.merge(method: 'GET')
32
+ fetch(url, **fetch_options)
33
+ end
34
+
35
+ def patch(url, **options)
36
+ fetch_options = options.merge(method: 'PATCH')
37
+ fetch(url, **fetch_options)
38
+ end
39
+
40
+ def put(url, **options)
41
+ fetch_options = options.merge(method: 'PUT')
42
+ fetch(url, **fetch_options)
43
+ end
44
+
45
+ def post(url, **options)
46
+ fetch_options = options.merge(method: 'POST')
47
+ fetch(url, **fetch_options)
48
+ end
49
+
50
+ def fetch(
51
+ urlOrRequest,
52
+ data: nil,
53
+ failOnStatusCode: nil,
54
+ form: nil,
55
+ headers: nil,
56
+ ignoreHTTPSErrors: nil,
57
+ maxRedirects: nil,
58
+ maxRetries: nil,
59
+ method: nil,
60
+ multipart: nil,
61
+ params: nil,
62
+ timeout: nil)
63
+
64
+ if [ChannelOwners::Request, String].none? { |type| urlOrRequest.is_a?(type) }
65
+ raise ArgumentError.new("First argument must be either URL string or Request")
66
+ end
67
+ if urlOrRequest.is_a?(ChannelOwners::Request)
68
+ request = urlOrRequest
69
+ url = nil
70
+ else
71
+ url = urlOrRequest
72
+ request = nil
73
+ end
74
+ _inner_fetch(
75
+ request,
76
+ url,
77
+ data: data,
78
+ failOnStatusCode: failOnStatusCode,
79
+ form: form,
80
+ headers: headers,
81
+ ignoreHTTPSErrors: ignoreHTTPSErrors,
82
+ maxRedirects: maxRedirects,
83
+ maxRetries: maxRetries,
84
+ method: method,
85
+ multipart: multipart,
86
+ params: params,
87
+ timeout: timeout,
88
+ )
89
+ end
90
+
91
+ private def _inner_fetch(
92
+ request,
93
+ url,
94
+ data: nil,
95
+ failOnStatusCode: nil,
96
+ form: nil,
97
+ headers: nil,
98
+ ignoreHTTPSErrors: nil,
99
+ maxRedirects: nil,
100
+ maxRetries: nil,
101
+ method: nil,
102
+ multipart: nil,
103
+ params: nil,
104
+ timeout: nil)
105
+ if @close_reason
106
+ raise TargetClosedError.new(message: @close_reason)
107
+ end
108
+ if [data, form, multipart].compact.count > 1
109
+ raise ArgumentError.new("Only one of 'data', 'form' or 'multipart' can be specified")
110
+ end
111
+ if maxRedirects && maxRedirects < 0
112
+ raise ArgumentError.new("'maxRedirects' should be greater than or equal to '0'")
113
+ end
114
+ if maxRetries && maxRetries < 0
115
+ raise ArgumentError.new("'maxRetries' should be greater than or equal to '0'")
116
+ end
117
+
118
+ headers_obj = headers || request&.headers
119
+ fetch_params = {
120
+ url: url || request.url,
121
+ params: map_params_to_array(params),
122
+ method: method || request&.method || 'GET',
123
+ headers: headers_obj ? HttpHeaders.new(headers_obj).as_serialized : nil,
124
+ }
125
+
126
+ json_data = nil
127
+ form_data = nil
128
+ multipart_data = nil
129
+ post_data_buffer = nil
130
+ if data
131
+ case data
132
+ when String
133
+ if headers_obj&.any? { |key, value| key.downcase == 'content-type' && value == 'application/json' }
134
+ json_data = json_parsable?(data) ? data : data.to_json
135
+ else
136
+ post_data_buffer = data
137
+ end
138
+ when Hash, Array, Numeric, true, false
139
+ json_data = data.to_json
140
+ else
141
+ raise ArgumentError.new("Unsupported 'data' type: #{data.class}")
142
+ end
143
+ elsif form
144
+ form_data = object_to_array(form)
145
+ elsif multipart
146
+ multipart_data = multipart.map do |name, value|
147
+ if file_payload?(value)
148
+ { name: name, file: file_payload_to_json(value) }
149
+ else
150
+ { name: name, value: value.to_s }
151
+ end
152
+ end
153
+ end
154
+
155
+ if !json_data && !form_data && !multipart_data
156
+ post_data_buffer ||= request&.post_data_buffer
157
+ end
158
+ if post_data_buffer
159
+ fetch_params[:postData] = Base64.strict_encode64(post_data_buffer)
160
+ end
161
+ fetch_params[:jsonData] = json_data
162
+ fetch_params[:formData] = form_data
163
+ fetch_params[:multipartData] = multipart_data
164
+ fetch_params[:timeout] = @timeout_settings.timeout(timeout)
165
+ fetch_params[:failOnStatusCode] = failOnStatusCode
166
+ fetch_params[:ignoreHTTPSErrors] = ignoreHTTPSErrors
167
+ fetch_params[:maxRedirects] = maxRedirects
168
+ fetch_params[:maxRetries] = maxRetries
169
+ fetch_params.compact!
170
+ response = @channel.send_message_to_server('fetch', fetch_params)
171
+
172
+ APIResponseImpl.new(self, response)
173
+ end
174
+
175
+ private def file_payload?(value)
176
+ value.is_a?(Hash) &&
177
+ %w(name mimeType buffer).all? { |key| value.has_key?(key) || value.has_key?(key.to_sym) }
178
+ end
179
+
180
+ private def file_payload_to_json(payload)
181
+ {
182
+ name: payload[:name] || payload['name'],
183
+ mimeType: payload[:mimeType] || payload['mimeType'],
184
+ buffer: Base64.strict_encode64(payload[:buffer] || payload['buffer'])
185
+ }
186
+ end
187
+
188
+ private def map_params_to_array(params)
189
+ if params.is_a?(String)
190
+ unless params.start_with?('?')
191
+ raise ArgumentError.new("Query string must start with '?'")
192
+ end
193
+ query_string_to_array(params[1..-1])
194
+ else
195
+ object_to_array(params)
196
+ end
197
+ end
198
+
199
+ private def query_string_to_array(query_string)
200
+ params = cgi_parse(query_string)
201
+
202
+ params.map do |key, values|
203
+ values.map do |value|
204
+ { name: key, value: value }
205
+ end
206
+ end.flatten
207
+ end
208
+
209
+ # https://bugs.ruby-lang.org/issues/21258
210
+ # CGI.parse is defined in 'cgi' library.
211
+ # But it produces an error in Ruby 2.4 environment: undefined method `delete_prefix' for "CONTENT_LENGTH":String
212
+ # So we implement our own version of CGI.parse here.
213
+ private def cgi_parse(query)
214
+ # https://github.com/ruby/cgi/blob/master/lib/cgi/core.rb#L396
215
+ params = {}
216
+
217
+ query.split(/[&;]/).each do |pairs|
218
+ key, value = pairs.split('=',2).map do |v|
219
+ CGI.unescape(v)
220
+ end
221
+
222
+ next unless key
223
+ params[key] ||= []
224
+ next unless value
225
+ params[key] << value
226
+ end
227
+
228
+ params
229
+ end
230
+
231
+ private def object_to_array(hash)
232
+ hash&.map do |key, value|
233
+ { name: key, value: value.to_s }
234
+ end
235
+ end
236
+
237
+ private def json_parsable?(data)
238
+ return false unless data.is_a?(String)
239
+ begin
240
+ JSON.parse(data)
241
+ true
242
+ rescue JSON::ParserError
243
+ false
244
+ end
245
+ end
246
+ end
247
+ end