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.
- checksums.yaml +4 -4
- data/AGENTS.md +4 -0
- data/CLAUDE/api_generation.md +28 -0
- data/CLAUDE/ci_expectations.md +23 -0
- data/CLAUDE/gem_release_flow.md +39 -0
- data/CLAUDE/past_upgrade_pr_patterns.md +42 -0
- data/CLAUDE/playwright_upgrade_workflow.md +35 -0
- data/CLAUDE/rspec_debugging.md +30 -0
- data/CLAUDE/unimplemented_examples.md +18 -0
- data/CLAUDE.md +32 -0
- data/CONTRIBUTING.md +5 -0
- data/README.md +60 -16
- data/documentation/README.md +33 -0
- data/documentation/babel.config.js +3 -0
- data/documentation/docs/api/api_request.md +7 -0
- data/documentation/docs/api/api_request_context.md +298 -0
- data/documentation/docs/api/api_response.md +114 -0
- data/documentation/docs/api/browser.md +237 -0
- data/documentation/docs/api/browser_context.md +503 -0
- data/documentation/docs/api/browser_type.md +184 -0
- data/documentation/docs/api/cdp_session.md +44 -0
- data/documentation/docs/api/clock.md +154 -0
- data/documentation/docs/api/console_message.md +85 -0
- data/documentation/docs/api/dialog.md +84 -0
- data/documentation/docs/api/download.md +111 -0
- data/documentation/docs/api/element_handle.md +694 -0
- data/documentation/docs/api/experimental/_category_.yml +3 -0
- data/documentation/docs/api/experimental/android.md +42 -0
- data/documentation/docs/api/experimental/android_device.md +109 -0
- data/documentation/docs/api/experimental/android_input.md +43 -0
- data/documentation/docs/api/experimental/android_socket.md +7 -0
- data/documentation/docs/api/experimental/android_web_view.md +7 -0
- data/documentation/docs/api/file_chooser.md +53 -0
- data/documentation/docs/api/frame.md +1218 -0
- data/documentation/docs/api/frame_locator.md +348 -0
- data/documentation/docs/api/js_handle.md +121 -0
- data/documentation/docs/api/keyboard.md +170 -0
- data/documentation/docs/api/locator.md +1495 -0
- data/documentation/docs/api/locator_assertions.md +827 -0
- data/documentation/docs/api/mouse.md +86 -0
- data/documentation/docs/api/page.md +1946 -0
- data/documentation/docs/api/page_assertions.md +65 -0
- data/documentation/docs/api/playwright.md +66 -0
- data/documentation/docs/api/request.md +255 -0
- data/documentation/docs/api/response.md +176 -0
- data/documentation/docs/api/route.md +205 -0
- data/documentation/docs/api/selectors.md +63 -0
- data/documentation/docs/api/touchscreen.md +22 -0
- data/documentation/docs/api/tracing.md +129 -0
- data/documentation/docs/api/web_socket.md +51 -0
- data/documentation/docs/api/worker.md +83 -0
- data/documentation/docs/article/api_coverage.mdx +11 -0
- data/documentation/docs/article/getting_started.md +161 -0
- data/documentation/docs/article/guides/_category_.yml +3 -0
- data/documentation/docs/article/guides/download_playwright_driver.md +55 -0
- data/documentation/docs/article/guides/inspector.md +31 -0
- data/documentation/docs/article/guides/launch_browser.md +121 -0
- data/documentation/docs/article/guides/playwright_on_alpine_linux.md +112 -0
- data/documentation/docs/article/guides/rails_integration.md +278 -0
- data/documentation/docs/article/guides/rails_integration_with_null_driver.md +145 -0
- data/documentation/docs/article/guides/recording_video.md +79 -0
- data/documentation/docs/article/guides/rspec_integration.md +59 -0
- data/documentation/docs/article/guides/semi_automation.md +71 -0
- data/documentation/docs/article/guides/use_storage_state.md +78 -0
- data/documentation/docs/include/api_coverage.md +671 -0
- data/documentation/docusaurus.config.js +114 -0
- data/documentation/package.json +39 -0
- data/documentation/sidebars.js +15 -0
- data/documentation/src/components/HomepageFeatures.js +61 -0
- data/documentation/src/components/HomepageFeatures.module.css +13 -0
- data/documentation/src/css/custom.css +44 -0
- data/documentation/src/pages/index.js +49 -0
- data/documentation/src/pages/index.module.css +41 -0
- data/documentation/src/pages/markdown-page.md +7 -0
- data/documentation/static/.nojekyll +0 -0
- data/documentation/static/img/playwright-logo.svg +9 -0
- data/documentation/static/img/playwright-ruby-client.png +0 -0
- data/documentation/static/img/undraw_dropdown_menu.svg +1 -0
- data/documentation/static/img/undraw_web_development.svg +1 -0
- data/documentation/static/img/undraw_windows.svg +1 -0
- data/documentation/yarn.lock +9005 -0
- data/lib/playwright/{input_types/android_input.rb → android_input_impl.rb} +5 -1
- data/lib/playwright/api_implementation.rb +18 -0
- data/lib/playwright/api_response_impl.rb +77 -0
- data/lib/playwright/channel.rb +62 -1
- data/lib/playwright/channel_owner.rb +70 -7
- data/lib/playwright/channel_owners/android.rb +16 -3
- data/lib/playwright/channel_owners/android_device.rb +22 -66
- data/lib/playwright/channel_owners/api_request_context.rb +247 -0
- data/lib/playwright/channel_owners/artifact.rb +40 -0
- data/lib/playwright/channel_owners/binding_call.rb +70 -0
- data/lib/playwright/channel_owners/browser.rb +114 -22
- data/lib/playwright/channel_owners/browser_context.rb +589 -15
- data/lib/playwright/channel_owners/browser_type.rb +90 -1
- data/lib/playwright/channel_owners/cdp_session.rb +19 -0
- data/lib/playwright/channel_owners/dialog.rb +32 -0
- data/lib/playwright/channel_owners/element_handle.rb +107 -43
- data/lib/playwright/channel_owners/fetch_request.rb +8 -0
- data/lib/playwright/channel_owners/frame.rb +334 -104
- data/lib/playwright/channel_owners/js_handle.rb +9 -13
- data/lib/playwright/channel_owners/local_utils.rb +82 -0
- data/lib/playwright/channel_owners/page.rb +778 -95
- data/lib/playwright/channel_owners/playwright.rb +25 -30
- data/lib/playwright/channel_owners/request.rb +120 -18
- data/lib/playwright/channel_owners/response.rb +113 -0
- data/lib/playwright/channel_owners/route.rb +181 -0
- data/lib/playwright/channel_owners/stream.rb +30 -0
- data/lib/playwright/channel_owners/tracing.rb +117 -0
- data/lib/playwright/channel_owners/web_socket.rb +96 -0
- data/lib/playwright/channel_owners/worker.rb +46 -0
- data/lib/playwright/channel_owners/writable_stream.rb +14 -0
- data/lib/playwright/clock_impl.rb +67 -0
- data/lib/playwright/connection.rb +111 -63
- data/lib/playwright/console_message_impl.rb +29 -0
- data/lib/playwright/download_impl.rb +32 -0
- data/lib/playwright/errors.rb +42 -5
- data/lib/playwright/event_emitter.rb +17 -3
- data/lib/playwright/event_emitter_proxy.rb +49 -0
- data/lib/playwright/events.rb +10 -5
- data/lib/playwright/file_chooser_impl.rb +24 -0
- data/lib/playwright/frame_locator_impl.rb +66 -0
- data/lib/playwright/har_router.rb +89 -0
- data/lib/playwright/http_headers.rb +14 -0
- data/lib/playwright/input_files.rb +102 -15
- data/lib/playwright/javascript/expression.rb +7 -11
- data/lib/playwright/javascript/regex.rb +23 -0
- data/lib/playwright/javascript/source_url.rb +16 -0
- data/lib/playwright/javascript/value_parser.rb +108 -19
- data/lib/playwright/javascript/value_serializer.rb +47 -8
- data/lib/playwright/javascript/visitor_info.rb +26 -0
- data/lib/playwright/javascript.rb +2 -10
- data/lib/playwright/{input_types/keyboard.rb → keyboard_impl.rb} +6 -2
- data/lib/playwright/locator_assertions_impl.rb +571 -0
- data/lib/playwright/locator_impl.rb +544 -0
- data/lib/playwright/locator_utils.rb +136 -0
- data/lib/playwright/mouse_impl.rb +57 -0
- data/lib/playwright/page_assertions_impl.rb +154 -0
- data/lib/playwright/playwright_api.rb +102 -30
- data/lib/playwright/raw_headers.rb +61 -0
- data/lib/playwright/route_handler.rb +78 -0
- data/lib/playwright/select_option_values.rb +34 -13
- data/lib/playwright/selectors_impl.rb +45 -0
- data/lib/playwright/test.rb +102 -0
- data/lib/playwright/timeout_settings.rb +9 -4
- data/lib/playwright/touchscreen_impl.rb +14 -0
- data/lib/playwright/transport.rb +61 -10
- data/lib/playwright/url_matcher.rb +24 -2
- data/lib/playwright/utils.rb +48 -13
- data/lib/playwright/version.rb +2 -1
- data/lib/playwright/video.rb +54 -0
- data/lib/playwright/waiter.rb +166 -0
- data/lib/playwright/web_socket_client.rb +167 -0
- data/lib/playwright/web_socket_transport.rb +116 -0
- data/lib/playwright.rb +188 -11
- data/lib/playwright_api/android.rb +46 -11
- data/lib/playwright_api/android_device.rb +182 -31
- data/lib/playwright_api/android_input.rb +22 -13
- data/lib/playwright_api/android_socket.rb +18 -0
- data/lib/playwright_api/android_web_view.rb +24 -0
- data/lib/playwright_api/api_request.rb +26 -0
- data/lib/playwright_api/api_request_context.rb +311 -0
- data/lib/playwright_api/api_response.rb +92 -0
- data/lib/playwright_api/browser.rb +116 -103
- data/lib/playwright_api/browser_context.rb +290 -389
- data/lib/playwright_api/browser_type.rb +96 -118
- data/lib/playwright_api/cdp_session.rb +36 -39
- data/lib/playwright_api/clock.rb +121 -0
- data/lib/playwright_api/console_message.rb +35 -19
- data/lib/playwright_api/dialog.rb +53 -50
- data/lib/playwright_api/download.rb +49 -43
- data/lib/playwright_api/element_handle.rb +354 -402
- data/lib/playwright_api/file_chooser.rb +15 -18
- data/lib/playwright_api/frame.rb +703 -603
- data/lib/playwright_api/frame_locator.rb +285 -0
- data/lib/playwright_api/js_handle.rb +50 -76
- data/lib/playwright_api/keyboard.rb +67 -146
- data/lib/playwright_api/locator.rb +1304 -0
- data/lib/playwright_api/locator_assertions.rb +704 -0
- data/lib/playwright_api/mouse.rb +23 -29
- data/lib/playwright_api/page.rb +1196 -1176
- data/lib/playwright_api/page_assertions.rb +60 -0
- data/lib/playwright_api/playwright.rb +54 -122
- data/lib/playwright_api/request.rb +112 -74
- data/lib/playwright_api/response.rb +92 -20
- data/lib/playwright_api/route.rb +152 -62
- data/lib/playwright_api/selectors.rb +47 -61
- data/lib/playwright_api/touchscreen.rb +8 -2
- data/lib/playwright_api/tracing.rb +128 -0
- data/lib/playwright_api/web_socket.rb +43 -5
- data/lib/playwright_api/worker.rb +74 -34
- data/playwright.gemspec +14 -9
- data/sig/playwright.rbs +658 -0
- metadata +216 -50
- data/docs/api_coverage.md +0 -354
- data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
- data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
- data/lib/playwright/channel_owners/console_message.rb +0 -21
- data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
- data/lib/playwright/channel_owners/selectors.rb +0 -4
- data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
- data/lib/playwright/input_type.rb +0 -19
- data/lib/playwright/input_types/mouse.rb +0 -4
- data/lib/playwright/input_types/touchscreen.rb +0 -4
- data/lib/playwright/javascript/function.rb +0 -67
- data/lib/playwright/wait_helper.rb +0 -73
- data/lib/playwright_api/accessibility.rb +0 -93
- data/lib/playwright_api/binding_call.rb +0 -23
- data/lib/playwright_api/chromium_browser_context.rb +0 -57
- data/lib/playwright_api/video.rb +0 -24
|
@@ -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
|
data/lib/playwright/channel.rb
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
8
|
-
|
|
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 =
|
|
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] ? {
|
|
50
|
+
hasChild: selector[:hasChild] ? { androidSelector: to_selector_channel(selector[:hasChild][:selector]) } : nil,
|
|
45
51
|
hasDescendant: selector[:hasDescendant] ? {
|
|
46
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|