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
@@ -0,0 +1,102 @@
1
+ module Playwright
2
+ # this module is responsible for running playwright assertions and integrating
3
+ # with test frameworks.
4
+ module Test
5
+ @@expect_timeout = nil
6
+
7
+ def self.expect_timeout
8
+ @@expect_timeout || 5000 # default timeout is 5000ms
9
+ end
10
+
11
+ def self.expect_timeout=(timeout)
12
+ @@expect_timeout = timeout
13
+ end
14
+
15
+ def self.with_timeout(expect_timeout, &block)
16
+ old_timeout = @@expect_timeout
17
+ @@expect_timeout = expect_timeout
18
+ block.call
19
+ ensure
20
+ @@expect_timeout = old_timeout
21
+ end
22
+
23
+ # ref: https://github.com/microsoft/playwright-python/blob/main/playwright/sync_api/__init__.py#L90
24
+ class Expect
25
+ def initialize
26
+ @default_expect_timeout = ::Playwright::Test.expect_timeout
27
+ end
28
+
29
+ def call(actual, is_not)
30
+ case actual
31
+ when Page
32
+ PageAssertions.new(
33
+ PageAssertionsImpl.new(
34
+ actual,
35
+ @default_expect_timeout,
36
+ is_not,
37
+ nil,
38
+ )
39
+ )
40
+ when Locator
41
+ LocatorAssertions.new(
42
+ LocatorAssertionsImpl.new(
43
+ actual,
44
+ @default_expect_timeout,
45
+ is_not,
46
+ nil,
47
+ )
48
+ )
49
+ else
50
+ raise NotImplementedError.new("Only locator assertions are currently implemented")
51
+ end
52
+ end
53
+ end
54
+
55
+ module Matchers
56
+ class PlaywrightMatcher
57
+ def initialize(expectation_method, *args, **kwargs)
58
+ @method = expectation_method
59
+ @args = args
60
+ @kwargs = kwargs
61
+ end
62
+
63
+ def matches?(actual)
64
+ Expect.new.call(actual, false).send(@method, *@args, **@kwargs)
65
+ true
66
+ rescue AssertionError => e
67
+ @failure_message = e.full_message
68
+ false
69
+ end
70
+
71
+ def does_not_match?(actual)
72
+ Expect.new.call(actual, true).send(@method, *@args, **@kwargs)
73
+ true
74
+ rescue AssertionError => e
75
+ @failure_message = e.full_message
76
+ false
77
+ end
78
+
79
+ def failure_message
80
+ @failure_message
81
+ end
82
+
83
+ def failure_message_when_negated
84
+ @failure_message
85
+ end
86
+ end
87
+ end
88
+
89
+ ALL_ASSERTIONS = PageAssertions.instance_methods(false) + LocatorAssertions.instance_methods(false)
90
+
91
+ ALL_ASSERTIONS
92
+ .map(&:to_s)
93
+ .each do |method_name|
94
+ # to_be_visible => be_visible
95
+ # not_to_be_visible => not_be_visible
96
+ root_method_name = method_name.gsub("to_", "")
97
+ Matchers.send(:define_method, root_method_name) do |*args, **kwargs|
98
+ Matchers::PlaywrightMatcher.new(method_name, *args, **kwargs)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,6 +1,7 @@
1
1
  module Playwright
2
2
  class TimeoutSettings
3
3
  DEFAULT_TIMEOUT = 30000
4
+ DEFAULT_LAUNCH_TIMEOUT = 180000 # 3 minutes
4
5
 
5
6
  def initialize(parent = nil)
6
7
  @parent = parent
@@ -8,12 +9,16 @@ module Playwright
8
9
 
9
10
  attr_writer :default_timeout, :default_navigation_timeout
10
11
 
11
- def navigation_timeout
12
- @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
12
+ def navigation_timeout(timeout_override = nil)
13
+ timeout_override || @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
13
14
  end
14
15
 
15
- def timeout
16
- @default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
16
+ def timeout(timeout_override = nil)
17
+ timeout_override || @default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
18
+ end
19
+
20
+ def launch_timeout(timeout_override = nil)
21
+ timeout_override || @default_timeout || @parent&.launch_timeout || DEFAULT_LAUNCH_TIMEOUT
17
22
  end
18
23
  end
19
24
  end
@@ -0,0 +1,14 @@
1
+ module Playwright
2
+ define_api_implementation :TouchscreenImpl do
3
+ def initialize(channel)
4
+ @channel = channel
5
+ end
6
+
7
+ def tap_point(x, y)
8
+ @channel.send_message_to_server('touchscreenTap', {
9
+ x: x,
10
+ y: y,
11
+ })
12
+ end
13
+ end
14
+ end
@@ -8,47 +8,69 @@ module Playwright
8
8
  # ref: https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_transport.py
9
9
  class Transport
10
10
  # @param playwright_cli_executable_path [String] path to playwright-cli.
11
- # @param debug [Boolean]
12
11
  def initialize(playwright_cli_executable_path:)
13
12
  @driver_executable_path = playwright_cli_executable_path
14
13
  @debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
14
+ @mutex = Mutex.new
15
15
  end
16
16
 
17
17
  def on_message_received(&block)
18
18
  @on_message = block
19
19
  end
20
20
 
21
+ def on_driver_closed(&block)
22
+ @on_driver_closed = block
23
+ end
24
+
25
+ def on_driver_crashed(&block)
26
+ @on_driver_crashed = block
27
+ end
28
+
21
29
  class AlreadyDisconnectedError < StandardError ; end
22
30
 
23
31
  # @param message [Hash]
24
32
  def send_message(message)
25
33
  debug_send_message(message) if @debug
26
34
  msg = JSON.dump(message)
27
- @stdin.write([msg.size].pack('V')) # unsigned 32bit, little endian
28
- @stdin.write(msg)
29
- rescue Errno::EPIPE
35
+ @mutex.synchronize {
36
+ @stdin.write([msg.bytes.length].pack('V')) # unsigned 32bit, little endian, real byte size instead of chars
37
+ @stdin.write(msg) # write UTF-8 in binary mode as byte stream
38
+ }
39
+ rescue Errno::EPIPE, IOError
30
40
  raise AlreadyDisconnectedError.new('send_message failed')
31
41
  end
32
42
 
33
43
  # Terminate playwright-cli driver.
34
44
  def stop
35
45
  [@stdin, @stdout, @stderr].each { |io| io.close unless io.closed? }
46
+ @thread&.join
36
47
  end
37
48
 
38
49
  # Start `playwright-cli run-driver`
39
50
  #
40
51
  # @note This method blocks until playwright-cli exited. Consider using Thread or Future.
41
- def run
42
- @stdin, @stdout, @stderr, @thread = Open3.popen3(@driver_executable_path, 'run-driver')
52
+ def async_run
53
+ @stdin, @stdout, @stderr, @thread = run_driver_with_open3
54
+ @stdin.binmode # Ensure Strings are written 1:1 without encoding conversion, necessary for integer values
43
55
 
44
56
  Thread.new { handle_stdout }
45
57
  Thread.new { handle_stderr }
46
-
47
- @thread.join
48
58
  end
49
59
 
50
60
  private
51
61
 
62
+ def run_driver_with_open3
63
+ Open3.popen3("#{@driver_executable_path} run-driver", { pgroup: true })
64
+ rescue ArgumentError => err
65
+ # Windows doesn't accept pgroup parameter.
66
+ # ArgumentError: wrong exec option symbol: pgroup
67
+ if err.message =~ /pgroup/
68
+ Open3.popen3("#{@driver_executable_path} run-driver")
69
+ else
70
+ raise
71
+ end
72
+ end
73
+
52
74
  def handle_stdout(packet_size: 32_768)
53
75
  while chunk = @stdout.read(4)
54
76
  length = chunk.unpack1('V') # unsigned 32bit, little endian
@@ -65,22 +87,51 @@ module Playwright
65
87
  end
66
88
  rescue IOError
67
89
  # disconnected by remote.
90
+ @on_driver_closed&.call
68
91
  end
69
92
 
70
93
  def handle_stderr
71
94
  while err = @stderr.read
95
+ # sometimed driver crashes with the error below.
96
+ # --------
97
+ # undefined:1
98
+ # �
99
+ # ^
100
+
101
+ # SyntaxError: Unexpected token � in JSON at position 0
102
+ # at JSON.parse (<anonymous>)
103
+ # at Transport.transport.onmessage (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/cli/driver.js:42:73)
104
+ # at Immediate.<anonymous> (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/protocol/transport.js:74:26)
105
+ # at processImmediate (internal/timers.js:461:21)
106
+ if err.include?('undefined:1')
107
+ @on_driver_crashed&.call
108
+ break
109
+ end
72
110
  $stderr.write(err)
73
111
  end
74
112
  rescue IOError
75
113
  # disconnected by remote.
114
+ @on_driver_closed&.call
76
115
  end
77
116
 
78
117
  def debug_send_message(message)
79
- puts "\x1b[33mSEND>\x1b[0m#{message}"
118
+ metadata = message.delete(:metadata)
119
+ puts "\x1b[33mSEND>\x1b[0m#{shorten_double_quoted_string(message)}"
120
+ message[:metadata] = metadata
80
121
  end
81
122
 
82
123
  def debug_recv_message(message)
83
- puts "\x1b[33mRECV>\x1b[0m#{message}"
124
+ puts "\x1b[33mRECV>\x1b[0m#{shorten_double_quoted_string(message)}"
125
+ end
126
+
127
+ def shorten_double_quoted_string(message, maxlen: 512)
128
+ message.to_s.gsub(/"([^"]+)"/) do |str|
129
+ if $1.length > maxlen
130
+ "\"#{$1[0...maxlen]}...\""
131
+ else
132
+ str
133
+ end
134
+ end
84
135
  end
85
136
  end
86
137
  end
@@ -1,19 +1,41 @@
1
1
  module Playwright
2
2
  class UrlMatcher
3
3
  # @param url [String|Regexp]
4
- def initialize(url)
4
+ # @param base_url [String|nil]
5
+ def initialize(url, base_url:)
5
6
  @url = url
7
+ @base_url = base_url
8
+ end
9
+
10
+ def as_pattern
11
+ case @url
12
+ when String
13
+ { glob: @url }
14
+ when Regexp
15
+ regex = JavaScript::Regex.new(@url)
16
+ { regexSource: regex.source, regexFlags: regex.flag }
17
+ else
18
+ nil
19
+ end
6
20
  end
7
21
 
8
22
  def match?(target_url)
9
23
  case @url
10
24
  when String
11
- @url == target_url
25
+ joined_url == target_url || File.fnmatch?(@url, target_url)
12
26
  when Regexp
13
27
  @url.match?(target_url)
14
28
  else
15
29
  false
16
30
  end
17
31
  end
32
+
33
+ private def joined_url
34
+ if @base_url && !@url.start_with?('*')
35
+ URI.join(@base_url, @url).to_s
36
+ else
37
+ @url
38
+ end
39
+ end
18
40
  end
19
41
  end
@@ -1,35 +1,70 @@
1
+ require 'base64'
2
+
1
3
  module Playwright
2
4
  module Utils
3
5
  module PrepareBrowserContextOptions
4
6
  # @see https://github.com/microsoft/playwright/blob/5a2cfdbd47ed3c3deff77bb73e5fac34241f649d/src/client/browserContext.ts#L265
5
7
  private def prepare_browser_context_options(params)
6
- if params[:viewport] == 0
7
- params.delete(:viewport)
8
+ if params[:noViewport] == 0
9
+ params.delete(:noViewport)
8
10
  params[:noDefaultViewport] = true
9
11
  end
10
12
  if params[:extraHTTPHeaders]
11
- # TODO
13
+ params[:extraHTTPHeaders] = ::Playwright::HttpHeaders.new(params[:extraHTTPHeaders]).as_serialized
14
+ end
15
+ if params[:record_video_dir]
16
+ params[:recordVideo] = {
17
+ dir: params.delete(:record_video_dir)
18
+ }
19
+ if params[:record_video_size]
20
+ params[:recordVideo][:size] = params.delete(:record_video_size)
21
+ end
12
22
  end
13
23
  if params[:storageState].is_a?(String)
14
24
  params[:storageState] = JSON.parse(File.read(params[:storageState]))
15
25
  end
16
26
 
27
+ %i[colorScheme reducedMotion forcedColors contrast].each do |key|
28
+ if params[key] == 'null'
29
+ params[key] = 'no-override'
30
+ end
31
+ end
32
+
33
+ if params[:acceptDownloads] || params[:acceptDownloads] == false
34
+ params[:acceptDownloads] = params[:acceptDownloads] ? 'accept' : 'deny'
35
+ end
36
+
37
+ if params[:clientCertificates].is_a?(Array)
38
+ params[:clientCertificates] = params[:clientCertificates].filter_map do |item|
39
+ out_record = {
40
+ origin: item[:origin],
41
+ passphrase: item[:passphrase],
42
+ }
43
+
44
+ { pfxPath: 'pfx', certPath: 'cert', keyPath: 'key' }.each do |key, out_key|
45
+ if (filepath = item[key])
46
+ out_record[out_key] = Base64.encode64(File.read(filepath)) rescue ''
47
+ elsif (value = item[out_key.to_sym])
48
+ out_record[out_key] = value
49
+ end
50
+ end
51
+
52
+ out_record.compact!
53
+ next nil if out_record.empty?
54
+
55
+ out_record
56
+ end
57
+ end
58
+
17
59
  params
18
60
  end
19
61
  end
20
62
 
21
63
  module Errors
22
- module SafeCloseError
64
+ module TargetClosedErrorMethods
23
65
  # @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
66
+ private def target_closed_error?(err)
67
+ err.is_a?(TargetClosedError)
33
68
  end
34
69
  end
35
70
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.0.8'
4
+ VERSION = '1.58.1.alpha1'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.58.0'
5
6
  end
@@ -0,0 +1,54 @@
1
+ module Playwright
2
+ class Video
3
+ def initialize(page)
4
+ @page = page
5
+ @artifact = Concurrent::Promises.resolvable_future
6
+ if @page.closed?
7
+ on_page_closed
8
+ else
9
+ page.once('close', -> { on_page_closed })
10
+ end
11
+ end
12
+
13
+ private def on_page_closed
14
+ unless @artifact.resolved?
15
+ @artifact.reject('Page closed')
16
+ end
17
+ end
18
+
19
+ # called only from Page#on_video via send(:set_artifact, artifact)
20
+ private def set_artifact(artifact)
21
+ @artifact.fulfill(artifact)
22
+ end
23
+
24
+ def path
25
+ if @page.send(:remote_connection?)
26
+ raise 'Path is not available when using browserType.connect(). Use save_as() to save a local copy.'
27
+ end
28
+ wait_for_artifact_and do |artifact|
29
+ artifact.absolute_path
30
+ end
31
+ end
32
+
33
+ def save_as(path)
34
+ wait_for_artifact_and do |artifact|
35
+ artifact.save_as(path)
36
+ end
37
+ end
38
+
39
+ def delete
40
+ wait_for_artifact_and do |artifact|
41
+ artifact.delete
42
+ end
43
+ end
44
+
45
+ private def wait_for_artifact_and(&block)
46
+ artifact = @artifact.value!
47
+ unless artifact
48
+ raise 'Page did not produce any video frames'
49
+ end
50
+
51
+ block.call(artifact)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,166 @@
1
+ require 'securerandom'
2
+
3
+ module Playwright
4
+ # ref: https://github.com/microsoft/playwright-python/blob/v1.40.0/playwright/_impl/_waiter.py
5
+ # ref: https://github.com/microsoft/playwright/blob/v1.40.0/packages/playwright-core/src/client/waiter.ts
6
+ class Waiter
7
+ def initialize(channel_owner, wait_name:)
8
+ @result = Concurrent::Promises.resolvable_future
9
+ @wait_id = SecureRandom.hex(16)
10
+ @event = wait_name
11
+ @channel = channel_owner.channel
12
+ @registered_listeners = Set.new
13
+ @listeners_mutex = Mutex.new
14
+ @logs = []
15
+ wait_for_event_info_before
16
+ end
17
+
18
+ private def wait_for_event_info_before
19
+ @channel.async_send_message_to_server(
20
+ "waitForEventInfo",
21
+ {
22
+ "info": {
23
+ "waitId": @wait_id,
24
+ "phase": "before",
25
+ "event": @event,
26
+ }
27
+ },
28
+ )
29
+ end
30
+
31
+ private def wait_for_event_info_after(error: nil)
32
+ @channel.async_send_message_to_server(
33
+ "waitForEventInfo",
34
+ {
35
+ "info": {
36
+ "waitId": @wait_id,
37
+ "phase": "after",
38
+ "error": error,
39
+ }.compact,
40
+ },
41
+ )
42
+ end
43
+
44
+ def reject_on_event(emitter, event, error_or_proc, predicate: nil)
45
+ listener = -> (*args) {
46
+ if !predicate || predicate.call(*args)
47
+ if error_or_proc.respond_to?(:call)
48
+ reject(error_or_proc.call)
49
+ else
50
+ reject(error_or_proc)
51
+ end
52
+ end
53
+ }
54
+ register_listener(emitter, event, listener)
55
+
56
+ self
57
+ end
58
+
59
+ def reject_on_timeout(timeout_ms, message)
60
+ return if timeout_ms <= 0
61
+
62
+ Concurrent::Promises.schedule(timeout_ms / 1000.0) do
63
+ reject(TimeoutError.new(message: message))
64
+ end.rescue do |err|
65
+ puts err, err.backtrace
66
+ end
67
+
68
+ self
69
+ end
70
+
71
+ private def cleanup
72
+ listeners = @listeners_mutex.synchronize do
73
+ @registered_listeners.to_a.tap { @registered_listeners.clear }
74
+ end
75
+ listeners.each do |emitter, event, listener|
76
+ emitter.off(event, listener)
77
+ end
78
+ end
79
+
80
+ def force_fulfill(result)
81
+ fulfill(result)
82
+ end
83
+
84
+ def force_reject(error)
85
+ reject(error)
86
+ end
87
+
88
+ private def fulfill(result)
89
+ cleanup
90
+ return if @result.resolved?
91
+ @result.fulfill(result)
92
+ wait_for_event_info_after
93
+ end
94
+
95
+ private def reject(error)
96
+ cleanup
97
+ return if @result.resolved?
98
+ klass = error.is_a?(TimeoutError) ? TimeoutError : Error
99
+ ex = klass.new(message: "#{error.message}#{format_log_recording(@logs)}")
100
+ @result.reject(ex)
101
+ wait_for_event_info_after(error: ex)
102
+ end
103
+
104
+ # @param [Playwright::EventEmitter]
105
+ # @param
106
+ def wait_for_event(emitter, event, predicate: nil)
107
+ listener = -> (*args) {
108
+ begin
109
+ if !predicate || predicate.call(*args)
110
+ fulfill(args.first)
111
+ end
112
+ rescue => err
113
+ reject(err)
114
+ end
115
+ }
116
+ register_listener(emitter, event, listener)
117
+
118
+ self
119
+ end
120
+
121
+ private def register_listener(emitter, event, listener)
122
+ emitter.on(event, listener)
123
+ remove_later = false
124
+ @listeners_mutex.synchronize do
125
+ if @result.resolved?
126
+ remove_later = true
127
+ else
128
+ @registered_listeners << [emitter, event, listener]
129
+ end
130
+ end
131
+ emitter.off(event, listener) if remove_later
132
+ end
133
+
134
+ attr_reader :result
135
+
136
+ def log(message)
137
+ @logs << message
138
+ begin
139
+ @channel.async_send_message_to_server(
140
+ "waitForEventInfo",
141
+ {
142
+ "info": {
143
+ "waitId": @wait_id,
144
+ "phase": "log",
145
+ "message": message,
146
+ },
147
+ },
148
+ )
149
+ rescue => err
150
+ # ignore
151
+ end
152
+ end
153
+
154
+ # @param logs [Array<String>]
155
+ private def format_log_recording(logs)
156
+ return "" if logs.empty?
157
+
158
+ header = " logs "
159
+ header_length = 60
160
+ left_length = ((header_length - header.length) / 2.0).to_i
161
+ right_length = header_length - header.length - left_length
162
+ new_line = "\n"
163
+ "#{new_line}#{'=' * left_length}#{header}#{'=' * right_length}#{new_line}#{logs.join(new_line)}#{new_line}#{'=' * header_length}"
164
+ end
165
+ end
166
+ end