playwright-ruby-client 0.0.4 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,67 @@
1
+ module Playwright
2
+ module JavaScript
3
+ class Function
4
+ def initialize(definition, arg)
5
+ @definition = definition
6
+ @serialized_arg = ValueSerializer.new(arg).serialize
7
+ end
8
+
9
+ def evaluate(channel)
10
+ value = channel.send_message_to_server(
11
+ 'evaluateExpression',
12
+ expression: @definition,
13
+ isFunction: true,
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: @definition,
23
+ isFunction: true,
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: @definition,
34
+ isFunction: true,
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: @definition,
45
+ isFunction: true,
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: @definition,
54
+ isFunction: true,
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
@@ -0,0 +1,75 @@
1
+ require 'date'
2
+
3
+ module Playwright
4
+ module JavaScript
5
+ class ValueParser
6
+ def initialize(hash)
7
+ @hash = hash
8
+ end
9
+
10
+ # @return [Hash]
11
+ def parse
12
+ if @hash.nil?
13
+ nil
14
+ else
15
+ parse_hash(@hash)
16
+ end
17
+ end
18
+
19
+ # ref: https://github.com/microsoft/playwright/blob/b45905ae3f1a066a8ecb358035ce745ddd21cf3a/src/protocol/serializers.ts#L42
20
+ # ref: https://github.com/microsoft/playwright-python/blob/25a99d53e00e35365cf5113b9525272628c0e65f/playwright/_impl/_js_handle.py#L140
21
+ private def parse_hash(hash)
22
+ %w(n s b).each do |key|
23
+ return hash[key] if hash.key?(key)
24
+ end
25
+
26
+ if hash.key?('v')
27
+ return
28
+ case hash['v']
29
+ when 'undefined'
30
+ nil
31
+ when 'null'
32
+ nil
33
+ when 'NaN'
34
+ Float::NAN
35
+ when 'Infinity'
36
+ Float::INFINITY
37
+ when '-Infinity'
38
+ -Float::INFINITY
39
+ when '-0'
40
+ -0
41
+ end
42
+ end
43
+
44
+ if hash.key?('d')
45
+ return DateTime.parse(hash['d'])
46
+ end
47
+
48
+ if hash.key?('r')
49
+ # @see https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp
50
+ # @see https://docs.ruby-lang.org/ja/latest/class/Regexp.html
51
+ js_regex_flag = hash['r']['f']
52
+ flags = []
53
+ flags << Regexp::IGNORECASE if js_regex_flag.include?('i')
54
+ flags << Regexp::MULTILINE if js_regex_flag.include?('m') || js_regex_flag.include?('s')
55
+
56
+ return Regexp.compile(hash['r']['p'], flags.inject(:|))
57
+ end
58
+
59
+ if hash.key?('a')
60
+ return hash['a'].map { |value| parse_hash(value) }
61
+ end
62
+
63
+ if hash.key?('o')
64
+ return hash['o'].map { |obj| [obj['k'], parse_hash(obj['v'])] }.to_h
65
+ end
66
+
67
+ if hash.key?('h')
68
+ return @handles[hash['h']]
69
+ end
70
+
71
+ raise ArgumentError.new("Unexpected value: #{hash}")
72
+ end
73
+ end
74
+ end
75
+ end
@@ -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(channel_owner)
17
- channel_owner_class_name = channel_owner.class.name
18
- raise "#{channel_owner_class_name} is not a channel owner" unless channel_owner_class_name.include?('::ChannelOwners::')
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
- @channel_owner = channel_owner
20
+ @impl = impl
21
+ @module_name = module_name
21
22
  end
22
23
 
23
24
  def create
24
- api_class = detect_class_for(@channel_owner.class)
25
+ api_class = detect_class_for(@impl.class)
25
26
  if api_class
26
- api_class.new(@channel_owner)
27
+ api_class.new(@impl)
27
28
  else
28
- raise NotImplementedError.new("Playwright::#{expected_class_name_for(@channel_owner.class)} is not implemented")
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('::ChannelOwners::').last
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
- if [::Playwright::ChannelOwner, Object].include?(klass.superclass)
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 channel_owner [Playwright::ChannelOwner]
53
- def initialize(channel_owner)
54
- @channel_owner = channel_owner
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| wrap_channel_owner(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 wrap_channel_owner(object)
66
- if object.is_a?(ChannelOwner)
74
+ private def wrap_impl(object)
75
+ case object
76
+ when ChannelOwner
67
77
  PlaywrightApi.from_channel_owner(object)
68
- elsif object.is_a?(Array)
69
- object.map { |obj| wrap_channel_owner(obj) }
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
+ params[:extraHTTPHeaders] = ::Playwright::HttpHeaders.new(params[:extraHTTPHeaders]).as_serialized
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.9'
5
5
  end
@@ -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