playwright-ruby-client 0.0.3 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +119 -12
- data/docs/api_coverage.md +354 -0
- data/lib/playwright.rb +8 -0
- data/lib/playwright/channel_owner.rb +16 -2
- data/lib/playwright/channel_owners/android.rb +10 -1
- data/lib/playwright/channel_owners/android_device.rb +163 -0
- data/lib/playwright/channel_owners/browser.rb +22 -29
- data/lib/playwright/channel_owners/browser_context.rb +43 -0
- data/lib/playwright/channel_owners/console_message.rb +21 -0
- data/lib/playwright/channel_owners/element_handle.rb +314 -0
- data/lib/playwright/channel_owners/frame.rb +466 -7
- data/lib/playwright/channel_owners/js_handle.rb +55 -0
- data/lib/playwright/channel_owners/page.rb +353 -5
- data/lib/playwright/channel_owners/request.rb +90 -0
- data/lib/playwright/channel_owners/webkit_browser.rb +1 -1
- data/lib/playwright/connection.rb +15 -14
- data/lib/playwright/errors.rb +1 -1
- data/lib/playwright/event_emitter.rb +13 -0
- data/lib/playwright/input_files.rb +42 -0
- data/lib/playwright/input_type.rb +19 -0
- data/lib/playwright/input_types/android_input.rb +19 -0
- data/lib/playwright/input_types/keyboard.rb +32 -0
- data/lib/playwright/input_types/mouse.rb +4 -0
- data/lib/playwright/input_types/touchscreen.rb +4 -0
- data/lib/playwright/javascript.rb +13 -0
- data/lib/playwright/javascript/expression.rb +67 -0
- data/lib/playwright/javascript/function.rb +67 -0
- data/lib/playwright/javascript/value_parser.rb +75 -0
- data/lib/playwright/javascript/value_serializer.rb +54 -0
- data/lib/playwright/playwright_api.rb +45 -25
- data/lib/playwright/select_option_values.rb +32 -0
- data/lib/playwright/timeout_settings.rb +19 -0
- data/lib/playwright/url_matcher.rb +19 -0
- data/lib/playwright/utils.rb +37 -0
- data/lib/playwright/version.rb +1 -1
- data/lib/playwright/wait_helper.rb +73 -0
- data/lib/playwright_api/accessibility.rb +60 -6
- data/lib/playwright_api/android.rb +33 -0
- data/lib/playwright_api/android_device.rb +78 -0
- data/lib/playwright_api/android_input.rb +25 -0
- data/lib/playwright_api/binding_call.rb +18 -0
- data/lib/playwright_api/browser.rb +136 -44
- data/lib/playwright_api/browser_context.rb +378 -51
- data/lib/playwright_api/browser_type.rb +137 -55
- data/lib/playwright_api/cdp_session.rb +32 -7
- data/lib/playwright_api/chromium_browser_context.rb +31 -0
- data/lib/playwright_api/console_message.rb +27 -7
- data/lib/playwright_api/dialog.rb +47 -3
- data/lib/playwright_api/download.rb +29 -5
- data/lib/playwright_api/element_handle.rb +429 -143
- data/lib/playwright_api/file_chooser.rb +13 -2
- data/lib/playwright_api/frame.rb +633 -179
- data/lib/playwright_api/js_handle.rb +97 -17
- data/lib/playwright_api/keyboard.rb +152 -24
- data/lib/playwright_api/mouse.rb +28 -3
- data/lib/playwright_api/page.rb +1183 -317
- data/lib/playwright_api/playwright.rb +174 -13
- data/lib/playwright_api/request.rb +115 -30
- data/lib/playwright_api/response.rb +22 -3
- data/lib/playwright_api/route.rb +63 -4
- data/lib/playwright_api/selectors.rb +29 -7
- data/lib/playwright_api/touchscreen.rb +2 -1
- data/lib/playwright_api/video.rb +11 -1
- data/lib/playwright_api/web_socket.rb +5 -5
- data/lib/playwright_api/worker.rb +29 -5
- data/playwright.gemspec +3 -0
- metadata +68 -2
@@ -0,0 +1,54 @@
|
|
1
|
+
module Playwright
|
2
|
+
module JavaScript
|
3
|
+
class ValueSerializer
|
4
|
+
def initialize(ruby_value)
|
5
|
+
@value = ruby_value
|
6
|
+
end
|
7
|
+
|
8
|
+
# @return [Hash]
|
9
|
+
def serialize
|
10
|
+
@handles = []
|
11
|
+
{ value: serialize_value(@value), handles: @handles }
|
12
|
+
end
|
13
|
+
|
14
|
+
# ref: https://github.com/microsoft/playwright/blob/b45905ae3f1a066a8ecb358035ce745ddd21cf3a/src/protocol/serializers.ts#L84
|
15
|
+
# ref: https://github.com/microsoft/playwright-python/blob/25a99d53e00e35365cf5113b9525272628c0e65f/playwright/_impl/_js_handle.py#L99
|
16
|
+
private def serialize_value(value)
|
17
|
+
case value
|
18
|
+
when ChannelOwners::JSHandle
|
19
|
+
index = @handles.count
|
20
|
+
@handles << value.channel
|
21
|
+
{ h: index }
|
22
|
+
when nil
|
23
|
+
{ v: 'undefined' }
|
24
|
+
when Float::NAN
|
25
|
+
{ v: 'NaN'}
|
26
|
+
when Float::INFINITY
|
27
|
+
{ v: 'Infinity' }
|
28
|
+
when -Float::INFINITY
|
29
|
+
{ v: '-Infinity' }
|
30
|
+
when true, false
|
31
|
+
{ b: value }
|
32
|
+
when Numeric
|
33
|
+
{ n: value }
|
34
|
+
when String
|
35
|
+
{ s: value }
|
36
|
+
when Time
|
37
|
+
require 'time'
|
38
|
+
{ d: value.utc.iso8601 }
|
39
|
+
when Regexp
|
40
|
+
flags = []
|
41
|
+
flags << 'ms' if (value.options & Regexp::MULTILINE) != 0
|
42
|
+
flags << 'i' if (value.options & Regexp::IGNORECASE) != 0
|
43
|
+
{ r: { p: value.source, f: flags.join('') } }
|
44
|
+
when Array
|
45
|
+
{ a: value.map { |v| serialize_value(v) } }
|
46
|
+
when Hash
|
47
|
+
{ o: value.map { |key, v| { k: key, v: serialize_value(v) } } }
|
48
|
+
else
|
49
|
+
raise ArgumentError.new("Unexpected value: #{value}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -7,66 +7,86 @@ module Playwright
|
|
7
7
|
# @param channel_owner [ChannelOwner]
|
8
8
|
# @note Intended for internal use only.
|
9
9
|
def self.from_channel_owner(channel_owner)
|
10
|
-
Factory.new(channel_owner).create
|
10
|
+
Factory.new(channel_owner, 'ChannelOwners').create
|
11
11
|
end
|
12
12
|
|
13
|
-
private
|
14
|
-
|
15
13
|
class Factory
|
16
|
-
def initialize(
|
17
|
-
|
18
|
-
|
14
|
+
def initialize(impl, module_name)
|
15
|
+
impl_class_name = impl.class.name
|
16
|
+
unless impl_class_name.include?("::#{module_name}::")
|
17
|
+
raise "#{impl_class_name} is not #{module_name}"
|
18
|
+
end
|
19
19
|
|
20
|
-
@
|
20
|
+
@impl = impl
|
21
|
+
@module_name = module_name
|
21
22
|
end
|
22
23
|
|
23
24
|
def create
|
24
|
-
api_class = detect_class_for(@
|
25
|
+
api_class = detect_class_for(@impl.class)
|
25
26
|
if api_class
|
26
|
-
api_class.new(@
|
27
|
+
api_class.new(@impl)
|
27
28
|
else
|
28
|
-
raise NotImplementedError.new("Playwright::#{expected_class_name_for(@
|
29
|
+
raise NotImplementedError.new("Playwright::#{expected_class_name_for(@impl.class)} is not implemented")
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
33
|
private
|
33
34
|
|
34
35
|
def expected_class_name_for(klass)
|
35
|
-
klass.name.split(
|
36
|
+
klass.name.split("::#{@module_name}::").last
|
37
|
+
end
|
38
|
+
|
39
|
+
def superclass_exist?(klass)
|
40
|
+
![::Playwright::ChannelOwner, ::Playwright::InputType, Object].include?(klass.superclass)
|
36
41
|
end
|
37
42
|
|
38
43
|
def detect_class_for(klass)
|
39
44
|
class_name = expected_class_name_for(klass)
|
40
45
|
if ::Playwright.const_defined?(class_name)
|
41
46
|
::Playwright.const_get(class_name)
|
47
|
+
elsif superclass_exist?(klass)
|
48
|
+
detect_class_for(klass.superclass)
|
42
49
|
else
|
43
|
-
|
44
|
-
nil
|
45
|
-
else
|
46
|
-
detect_class_for(klass.superclass)
|
47
|
-
end
|
50
|
+
nil
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
52
|
-
# @param
|
53
|
-
def initialize(
|
54
|
-
@
|
55
|
+
# @param impl [Playwright::ChannelOwner|Playwright::InputType]
|
56
|
+
def initialize(impl)
|
57
|
+
@impl = impl
|
58
|
+
end
|
59
|
+
|
60
|
+
def ==(other)
|
61
|
+
@impl.to_s == other.instance_variable_get(:'@impl').to_s
|
55
62
|
end
|
56
63
|
|
57
64
|
# @param block [Proc]
|
58
|
-
def wrap_block_call(block)
|
65
|
+
private def wrap_block_call(block)
|
66
|
+
return nil unless block.is_a?(Proc)
|
67
|
+
|
59
68
|
-> (*args) {
|
60
|
-
wrapped_args = args.map { |arg|
|
69
|
+
wrapped_args = args.map { |arg| wrap_impl(arg) }
|
61
70
|
block.call(*wrapped_args)
|
62
71
|
}
|
63
72
|
end
|
64
73
|
|
65
|
-
def
|
66
|
-
|
74
|
+
private def wrap_impl(object)
|
75
|
+
case object
|
76
|
+
when ChannelOwner
|
67
77
|
PlaywrightApi.from_channel_owner(object)
|
68
|
-
|
69
|
-
object
|
78
|
+
when InputType
|
79
|
+
Factory.new(object, 'InputTypes').create
|
80
|
+
when Array
|
81
|
+
object.map { |obj| wrap_impl(obj) }
|
82
|
+
else
|
83
|
+
object
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private def unwrap_impl(object)
|
88
|
+
if object.is_a?(PlaywrightApi)
|
89
|
+
object.instance_variable_get(:@impl)
|
70
90
|
else
|
71
91
|
object
|
72
92
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Playwright
|
2
|
+
class SelectOptionValues
|
3
|
+
def initialize(values)
|
4
|
+
@params = convert(values)
|
5
|
+
end
|
6
|
+
|
7
|
+
# @return [Hash]
|
8
|
+
def as_params
|
9
|
+
@params
|
10
|
+
end
|
11
|
+
|
12
|
+
private def convert(values)
|
13
|
+
return {} unless values
|
14
|
+
return convert([values]) unless values.is_a?('Array')
|
15
|
+
return {} if values.empty?
|
16
|
+
values.each_with_index do |value, index|
|
17
|
+
unless values
|
18
|
+
raise ArgumentError.new("options[#{index}]: expected object, got null")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
case values.first
|
23
|
+
when ElementHandle
|
24
|
+
{ elements: values.map(&:channel) }
|
25
|
+
when String
|
26
|
+
{ options: values.map { |value| { value: value } } }
|
27
|
+
else
|
28
|
+
{ options: values }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Playwright
|
2
|
+
class TimeoutSettings
|
3
|
+
DEFAULT_TIMEOUT = 30000
|
4
|
+
|
5
|
+
def initialize(parent = nil)
|
6
|
+
@parent = parent
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_writer :default_timeout, :default_navigation_timeout
|
10
|
+
|
11
|
+
def navigation_timeout
|
12
|
+
@default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
|
13
|
+
end
|
14
|
+
|
15
|
+
def timeout
|
16
|
+
@default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Playwright
|
2
|
+
class UrlMatcher
|
3
|
+
# @param url [String|Regexp]
|
4
|
+
def initialize(url)
|
5
|
+
@url = url
|
6
|
+
end
|
7
|
+
|
8
|
+
def match?(target_url)
|
9
|
+
case @url
|
10
|
+
when String
|
11
|
+
@url == target_url
|
12
|
+
when Regexp
|
13
|
+
@url.match?(target_url)
|
14
|
+
else
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Playwright
|
2
|
+
module Utils
|
3
|
+
module PrepareBrowserContextOptions
|
4
|
+
# @see https://github.com/microsoft/playwright/blob/5a2cfdbd47ed3c3deff77bb73e5fac34241f649d/src/client/browserContext.ts#L265
|
5
|
+
private def prepare_browser_context_options(params)
|
6
|
+
if params[:viewport] == 0
|
7
|
+
params.delete(:viewport)
|
8
|
+
params[:noDefaultViewport] = true
|
9
|
+
end
|
10
|
+
if params[:extraHTTPHeaders]
|
11
|
+
# TODO
|
12
|
+
end
|
13
|
+
if params[:storageState].is_a?(String)
|
14
|
+
params[:storageState] = JSON.parse(File.read(params[:storageState]))
|
15
|
+
end
|
16
|
+
|
17
|
+
params
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Errors
|
22
|
+
module SafeCloseError
|
23
|
+
# @param err [Exception]
|
24
|
+
private def safe_close_error?(err)
|
25
|
+
return true if err.is_a?(Transport::AlreadyDisconnectedError)
|
26
|
+
|
27
|
+
[
|
28
|
+
'Browser has been closed',
|
29
|
+
'Target page, context or browser has been closed',
|
30
|
+
].any? do |closed_message|
|
31
|
+
err.message.end_with?(closed_message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/playwright/version.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Playwright
|
2
|
+
# ref: https://github.com/microsoft/playwright-python/blob/30946ae3099d51f9b7f355f9ae7e8c04d748ce36/playwright/_impl/_wait_helper.py
|
3
|
+
# ref: https://github.com/microsoft/playwright/blob/01fb3a6045cbdb4b5bcba0809faed85bd917ab87/src/client/waiter.ts#L21
|
4
|
+
class WaitHelper
|
5
|
+
def initialize
|
6
|
+
@promise = Concurrent::Promises.resolvable_future
|
7
|
+
@registered_listeners = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def reject_on_event(emitter, event, error, predicate: nil)
|
11
|
+
listener = -> (*args) {
|
12
|
+
if !predicate || predicate.call(*args)
|
13
|
+
reject(error)
|
14
|
+
end
|
15
|
+
}
|
16
|
+
emitter.on(event, listener)
|
17
|
+
@registered_listeners << [emitter, event, listener]
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def reject_on_timeout(timeout_ms, message)
|
23
|
+
return if timeout_ms <= 0
|
24
|
+
|
25
|
+
Concurrent::Promises.schedule(timeout_ms / 1000.0) {
|
26
|
+
reject(TimeoutError.new(message))
|
27
|
+
}
|
28
|
+
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [Playwright::EventEmitter]
|
33
|
+
# @param
|
34
|
+
def wait_for_event(emitter, event, predicate: nil)
|
35
|
+
listener = -> (*args) {
|
36
|
+
begin
|
37
|
+
if !predicate || predicate.call(*args)
|
38
|
+
fulfill(*args)
|
39
|
+
end
|
40
|
+
rescue => err
|
41
|
+
reject(err)
|
42
|
+
end
|
43
|
+
}
|
44
|
+
emitter.on(event, listener)
|
45
|
+
@registered_listeners << [emitter, event, listener]
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :promise
|
51
|
+
|
52
|
+
private def cleanup
|
53
|
+
@registered_listeners.each do |emitter, event, listener|
|
54
|
+
emitter.off(event, listener)
|
55
|
+
end
|
56
|
+
@registered_listeners.clear
|
57
|
+
end
|
58
|
+
|
59
|
+
private def fulfill(*args)
|
60
|
+
cleanup
|
61
|
+
unless @promise.resolved?
|
62
|
+
@promise.fulfill(args.first)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private def reject(error)
|
67
|
+
cleanup
|
68
|
+
unless @promise.resolved?
|
69
|
+
@promise.reject(error)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -1,21 +1,45 @@
|
|
1
1
|
module Playwright
|
2
|
-
# The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
2
|
+
# The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by
|
3
|
+
# assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or
|
4
|
+
# [switches](https://en.wikipedia.org/wiki/Switch_access).
|
5
|
+
#
|
6
|
+
# Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might
|
7
|
+
# have wildly different output.
|
8
|
+
#
|
9
|
+
# Rendering engines of Chromium, Firefox and Webkit have a concept of "accessibility tree", which is then translated into
|
10
|
+
# different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree.
|
11
|
+
#
|
12
|
+
# Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific
|
13
|
+
# AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing
|
14
|
+
# only the "interesting" nodes of the tree.
|
6
15
|
class Accessibility < PlaywrightApi
|
7
16
|
|
8
|
-
# Captures the current state of the accessibility tree. The returned object represents the root accessible node of the
|
17
|
+
# Captures the current state of the accessibility tree. The returned object represents the root accessible node of the
|
18
|
+
# page.
|
9
19
|
#
|
10
|
-
#
|
20
|
+
# > NOTE: The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers.
|
21
|
+
# Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to `false`.
|
11
22
|
#
|
12
23
|
# An example of dumping the entire accessibility tree:
|
24
|
+
#
|
13
25
|
#
|
14
26
|
# ```js
|
15
27
|
# const snapshot = await page.accessibility.snapshot();
|
16
28
|
# console.log(snapshot);
|
17
29
|
# ```
|
30
|
+
#
|
31
|
+
# ```python async
|
32
|
+
# snapshot = await page.accessibility.snapshot()
|
33
|
+
# print(snapshot)
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
# ```python sync
|
37
|
+
# snapshot = page.accessibility.snapshot()
|
38
|
+
# print(snapshot)
|
39
|
+
# ```
|
40
|
+
#
|
18
41
|
# An example of logging the focused node's name:
|
42
|
+
#
|
19
43
|
#
|
20
44
|
# ```js
|
21
45
|
# const snapshot = await page.accessibility.snapshot();
|
@@ -32,6 +56,36 @@ module Playwright
|
|
32
56
|
# return null;
|
33
57
|
# }
|
34
58
|
# ```
|
59
|
+
#
|
60
|
+
# ```python async
|
61
|
+
# def find_focused_node(node):
|
62
|
+
# if (node.get("focused"))
|
63
|
+
# return node
|
64
|
+
# for child in (node.get("children") or []):
|
65
|
+
# found_node = find_focused_node(child)
|
66
|
+
# return found_node
|
67
|
+
# return None
|
68
|
+
#
|
69
|
+
# snapshot = await page.accessibility.snapshot()
|
70
|
+
# node = find_focused_node(snapshot)
|
71
|
+
# if node:
|
72
|
+
# print(node["name"])
|
73
|
+
# ```
|
74
|
+
#
|
75
|
+
# ```python sync
|
76
|
+
# def find_focused_node(node):
|
77
|
+
# if (node.get("focused"))
|
78
|
+
# return node
|
79
|
+
# for child in (node.get("children") or []):
|
80
|
+
# found_node = find_focused_node(child)
|
81
|
+
# return found_node
|
82
|
+
# return None
|
83
|
+
#
|
84
|
+
# snapshot = page.accessibility.snapshot()
|
85
|
+
# node = find_focused_node(snapshot)
|
86
|
+
# if node:
|
87
|
+
# print(node["name"])
|
88
|
+
# ```
|
35
89
|
def snapshot(interestingOnly: nil, root: nil)
|
36
90
|
raise NotImplementedError.new('snapshot is not implemented yet.')
|
37
91
|
end
|