playwright-ruby-client 0.7.0 → 0.9.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -0
  3. data/documentation/docs/api/browser.md +18 -2
  4. data/documentation/docs/api/browser_context.md +10 -0
  5. data/documentation/docs/api/browser_type.md +1 -0
  6. data/documentation/docs/api/cdp_session.md +41 -1
  7. data/documentation/docs/api/download.md +97 -0
  8. data/documentation/docs/api/element_handle.md +38 -4
  9. data/documentation/docs/api/experimental/android_device.md +1 -0
  10. data/documentation/docs/api/frame.md +78 -17
  11. data/documentation/docs/api/keyboard.md +11 -20
  12. data/documentation/docs/api/locator.md +650 -0
  13. data/documentation/docs/api/page.md +107 -19
  14. data/documentation/docs/api/response.md +16 -0
  15. data/documentation/docs/article/guides/inspector.md +31 -0
  16. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +91 -0
  17. data/documentation/docs/article/guides/rails_integration.md +1 -1
  18. data/documentation/docs/article/guides/semi_automation.md +5 -1
  19. data/documentation/docs/include/api_coverage.md +70 -7
  20. data/lib/playwright.rb +36 -4
  21. data/lib/playwright/channel_owners/artifact.rb +4 -0
  22. data/lib/playwright/channel_owners/browser.rb +5 -0
  23. data/lib/playwright/channel_owners/browser_context.rb +37 -3
  24. data/lib/playwright/channel_owners/cdp_session.rb +19 -0
  25. data/lib/playwright/channel_owners/element_handle.rb +11 -4
  26. data/lib/playwright/channel_owners/frame.rb +103 -34
  27. data/lib/playwright/channel_owners/page.rb +140 -53
  28. data/lib/playwright/channel_owners/response.rb +9 -1
  29. data/lib/playwright/connection.rb +2 -4
  30. data/lib/playwright/{download.rb → download_impl.rb} +5 -1
  31. data/lib/playwright/javascript/expression.rb +5 -4
  32. data/lib/playwright/locator_impl.rb +314 -0
  33. data/lib/playwright/route_handler_entry.rb +3 -2
  34. data/lib/playwright/timeout_settings.rb +4 -4
  35. data/lib/playwright/transport.rb +0 -1
  36. data/lib/playwright/url_matcher.rb +12 -2
  37. data/lib/playwright/version.rb +2 -2
  38. data/lib/playwright/web_socket_client.rb +164 -0
  39. data/lib/playwright/web_socket_transport.rb +104 -0
  40. data/lib/playwright_api/android.rb +6 -6
  41. data/lib/playwright_api/android_device.rb +10 -9
  42. data/lib/playwright_api/browser.rb +17 -11
  43. data/lib/playwright_api/browser_context.rb +14 -9
  44. data/lib/playwright_api/browser_type.rb +8 -7
  45. data/lib/playwright_api/cdp_session.rb +30 -8
  46. data/lib/playwright_api/console_message.rb +6 -6
  47. data/lib/playwright_api/dialog.rb +6 -6
  48. data/lib/playwright_api/download.rb +70 -0
  49. data/lib/playwright_api/element_handle.rb +44 -24
  50. data/lib/playwright_api/frame.rb +100 -49
  51. data/lib/playwright_api/js_handle.rb +6 -6
  52. data/lib/playwright_api/locator.rb +509 -0
  53. data/lib/playwright_api/page.rb +110 -57
  54. data/lib/playwright_api/playwright.rb +6 -6
  55. data/lib/playwright_api/request.rb +6 -6
  56. data/lib/playwright_api/response.rb +15 -10
  57. data/lib/playwright_api/route.rb +6 -6
  58. data/lib/playwright_api/selectors.rb +6 -6
  59. data/lib/playwright_api/web_socket.rb +6 -6
  60. data/lib/playwright_api/worker.rb +6 -6
  61. metadata +15 -5
@@ -24,13 +24,14 @@ module Playwright
24
24
  ::Playwright::ChannelOwner.from(resp)
25
25
  end
26
26
 
27
- def eval_on_selector(channel, selector)
28
- value = channel.send_message_to_server(
29
- 'evalOnSelector',
27
+ def eval_on_selector(channel, selector, strict: nil)
28
+ params = {
30
29
  selector: selector,
31
30
  expression: @expression,
32
31
  arg: @serialized_arg,
33
- )
32
+ }
33
+ params[:strict] = strict if strict
34
+ value = channel.send_message_to_server('evalOnSelector', params)
34
35
  ValueParser.new(value).parse
35
36
  end
36
37
 
@@ -0,0 +1,314 @@
1
+ module Playwright
2
+ define_api_implementation :LocatorImpl do
3
+ def initialize(frame:, timeout_settings:, selector:)
4
+ @frame = frame
5
+ @timeout_settings = timeout_settings
6
+ @selector = selector
7
+ end
8
+
9
+ def to_s
10
+ "Locator@#{@selector}"
11
+ end
12
+
13
+ private def with_element(timeout: nil, &block)
14
+ start_time = Time.now
15
+
16
+ handle = @frame.wait_for_selector(@selector, strict: true, state: 'attached', timeout: timeout)
17
+ unless handle
18
+ raise "Could not resolve #{@selector} to DOM Element"
19
+ end
20
+
21
+ call_options = {}
22
+ if timeout
23
+ call_options[:timeout] = (timeout - (Time.now - start_time) * 1000).to_i
24
+ end
25
+
26
+ begin
27
+ block.call(handle, call_options)
28
+ ensure
29
+ handle.dispose
30
+ end
31
+ end
32
+
33
+ def bounding_box(timeout: nil)
34
+ with_element(timeout: timeout) do |handle|
35
+ handle.bounding_box
36
+ end
37
+ end
38
+
39
+ def check(
40
+ force: nil,
41
+ noWaitAfter: nil,
42
+ position: nil,
43
+ timeout: nil,
44
+ trial: nil)
45
+
46
+ @frame.check(@selector,
47
+ strict: true,
48
+ force: force,
49
+ noWaitAfter: noWaitAfter,
50
+ position: position,
51
+ timeout: timeout,
52
+ trial: trial)
53
+ end
54
+
55
+ def click(
56
+ button: nil,
57
+ clickCount: nil,
58
+ delay: nil,
59
+ force: nil,
60
+ modifiers: nil,
61
+ noWaitAfter: nil,
62
+ position: nil,
63
+ timeout: nil,
64
+ trial: nil)
65
+
66
+ @frame.click(@selector,
67
+ strict: true,
68
+ button: button,
69
+ clickCount: clickCount,
70
+ delay: delay,
71
+ force: force,
72
+ modifiers: modifiers,
73
+ noWaitAfter: noWaitAfter,
74
+ position: position,
75
+ timeout: timeout,
76
+ trial: trial)
77
+ end
78
+
79
+ def dblclick(
80
+ button: nil,
81
+ delay: nil,
82
+ force: nil,
83
+ modifiers: nil,
84
+ noWaitAfter: nil,
85
+ position: nil,
86
+ timeout: nil,
87
+ trial: nil)
88
+
89
+ @frame.dblclick(@selector,
90
+ strict: true,
91
+ button: button,
92
+ delay: delay,
93
+ force: force,
94
+ modifiers: modifiers,
95
+ noWaitAfter: noWaitAfter,
96
+ position: position,
97
+ timeout: timeout,
98
+ trial: trial)
99
+ end
100
+
101
+ def dispatch_event(type, eventInit: nil, timeout: nil)
102
+ @frame.dispatch_event(@selector, type, strict: true, eventInit: eventInit, timeout: timeout)
103
+ end
104
+
105
+ def evaluate(expression, arg: nil, timeout: nil)
106
+ with_element(timeout: timeout) do |handle|
107
+ handle.evaluate(expression, arg: arg)
108
+ end
109
+ end
110
+
111
+ def evaluate_all(expression, arg: nil)
112
+ @frame.eval_on_selector_all(@selector, expression, arg: arg)
113
+ end
114
+
115
+ def evaluate_handle(expression, arg: nil, timeout: nil)
116
+ with_element(timeout: timeout) do |handle|
117
+ handle.evaluate_handle(expression, arg: arg)
118
+ end
119
+ end
120
+
121
+ def fill(value, force: nil, noWaitAfter: nil, timeout: nil)
122
+ @frame.fill(@selector, value, strict: true, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
123
+ end
124
+
125
+ def locator(selector)
126
+ LocatorImpl.new(
127
+ frame: @frame,
128
+ timeout_settings: @timeout_settings,
129
+ selector: "#{@selector} >> #{selector}",
130
+ )
131
+ end
132
+
133
+ def element_handle(timeout: nil)
134
+ @frame.wait_for_selector(@selector, strict: true, state: 'attached', timeout: timeout)
135
+ end
136
+
137
+ def element_handles
138
+ @frame.query_selector_all(@selector)
139
+ end
140
+
141
+ def first
142
+ LocatorImpl.new(
143
+ frame: @frame,
144
+ timeout_settings: @timeout_settings,
145
+ selector: "#{@selector} >> _nth=first",
146
+ )
147
+ end
148
+
149
+ def last
150
+ LocatorImpl.new(
151
+ frame: @frame,
152
+ timeout_settings: @timeout_settings,
153
+ selector: "#{@selector} >> _nth=last",
154
+ )
155
+ end
156
+
157
+ def nth(index)
158
+ LocatorImpl.new(
159
+ frame: @frame,
160
+ timeout_settings: @timeout_settings,
161
+ selector: "#{@selector} >> _nth=#{index}",
162
+ )
163
+ end
164
+
165
+ def focus(timeout: nil)
166
+ @frame.focus(@selector, strict: true, timeout: timeout)
167
+ end
168
+
169
+ def count
170
+ @frame.eval_on_selector_all(@selector, 'ee => ee.length')
171
+ end
172
+
173
+ def get_attribute(name, timeout: nil)
174
+ @frame.get_attribute(@selector, name, strict: true, timeout: timeout)
175
+ end
176
+
177
+ def hover(
178
+ force: nil,
179
+ modifiers: nil,
180
+ position: nil,
181
+ timeout: nil,
182
+ trial: nil)
183
+ @frame.hover(@selector,
184
+ strict: true,
185
+ force: force,
186
+ modifiers: modifiers,
187
+ position: position,
188
+ timeout: timeout,
189
+ trial: trial)
190
+ end
191
+
192
+ def inner_html(timeout: nil)
193
+ @frame.inner_html(@selector, strict: true, timeout: timeout)
194
+ end
195
+
196
+ def inner_text(timeout: nil)
197
+ @frame.inner_text(@selector, strict: true, timeout: timeout)
198
+ end
199
+
200
+ def input_value(timeout: nil)
201
+ @frame.input_value(@selector, strict: true, timeout: timeout)
202
+ end
203
+
204
+ %i[checked? disabled? editable? enabled? hidden? visible?].each do |method_name|
205
+ define_method(method_name) do |timeout: nil|
206
+ @frame.public_send(method_name, @selector, strict: true, timeout: timeout)
207
+ end
208
+ end
209
+
210
+ def press(key, delay: nil, noWaitAfter: nil, timeout: nil)
211
+ @frame.press(@selector, key, strict: true, noWaitAfter: noWaitAfter, timeout: timeout)
212
+ end
213
+
214
+ def screenshot(
215
+ omitBackground: nil,
216
+ path: nil,
217
+ quality: nil,
218
+ timeout: nil,
219
+ type: nil)
220
+ with_element(timeout: timeout) do |handle, options|
221
+ handle.screenshot(
222
+ omitBackground: omitBackground,
223
+ path: path,
224
+ quality: quality,
225
+ timeout: options[:timeout],
226
+ type: type)
227
+ end
228
+ end
229
+
230
+ def scroll_into_view_if_needed(timeout: nil)
231
+ with_element(timeout: timeout) do |handle, options|
232
+ handle.scroll_into_view_if_needed(timeout: options[:timeout])
233
+ end
234
+ end
235
+
236
+ def select_option(
237
+ element: nil,
238
+ index: nil,
239
+ value: nil,
240
+ label: nil,
241
+ force: nil,
242
+ noWaitAfter: nil,
243
+ timeout: nil)
244
+
245
+ @frame.select_option(@selector,
246
+ strict: true,
247
+ element: element,
248
+ index: index,
249
+ value: value,
250
+ label: label,
251
+ force: force,
252
+ noWaitAfter: noWaitAfter,
253
+ timeout: timeout)
254
+ end
255
+
256
+ def select_text(force: nil, timeout: nil)
257
+ with_element(timeout: timeout) do |handle, options|
258
+ handle.select_text(force: force, timeout: options[:timeout])
259
+ end
260
+ end
261
+
262
+ def set_input_files(files, noWaitAfter: nil, timeout: nil)
263
+ @frame.set_input_files(@selector, files, strict: true, noWaitAfter: noWaitAfter, timeout: timeout)
264
+ end
265
+
266
+ def tap_point(
267
+ force: nil,
268
+ modifiers: nil,
269
+ noWaitAfter: nil,
270
+ position: nil,
271
+ timeout: nil,
272
+ trial: nil)
273
+ @frame.tap_point(@selector,
274
+ strict: true,
275
+ force: force,
276
+ modifiers: modifiers,
277
+ noWaitAfter: noWaitAfter,
278
+ position: position,
279
+ timeout: timeout,
280
+ trial: trial)
281
+ end
282
+
283
+ def text_content(timeout: nil)
284
+ @frame.text_content(@selector, strict: true, timeout: timeout)
285
+ end
286
+
287
+ def type(text, delay: nil, noWaitAfter: nil, timeout: nil)
288
+ @frame.type(@selector, text, strict: true, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
289
+ end
290
+
291
+ def uncheck(
292
+ force: nil,
293
+ noWaitAfter: nil,
294
+ position: nil,
295
+ timeout: nil,
296
+ trial: nil)
297
+ @frame.uncheck(@selector,
298
+ strict: true,
299
+ force: force,
300
+ noWaitAfter: noWaitAfter,
301
+ position: position,
302
+ timeout: timeout,
303
+ trial: trial)
304
+ end
305
+
306
+ def all_inner_texts
307
+ @frame.eval_on_selector_all(@selector, 'ee => ee.map(e => e.innerText)')
308
+ end
309
+
310
+ def all_text_contents
311
+ @frame.eval_on_selector_all(@selector, "ee => ee.map(e => e.textContent || '')")
312
+ end
313
+ end
314
+ end
@@ -1,10 +1,11 @@
1
1
  module Playwright
2
2
  class RouteHandlerEntry
3
3
  # @param url [String]
4
+ # @param base_url [String|nil]
4
5
  # @param handler [Proc]
5
- def initialize(url, handler)
6
+ def initialize(url, base_url, handler)
6
7
  @url_value = url
7
- @url_matcher = UrlMatcher.new(url)
8
+ @url_matcher = UrlMatcher.new(url, base_url: base_url)
8
9
  @handler = handler
9
10
  end
10
11
 
@@ -8,12 +8,12 @@ module Playwright
8
8
 
9
9
  attr_writer :default_timeout, :default_navigation_timeout
10
10
 
11
- def navigation_timeout
12
- @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
11
+ def navigation_timeout(timeout_override = nil)
12
+ timeout_override || @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
13
13
  end
14
14
 
15
- def timeout
16
- @default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
15
+ def timeout(timeout_override = nil)
16
+ timeout_override || @default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
17
17
  end
18
18
  end
19
19
  end
@@ -8,7 +8,6 @@ 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'
@@ -1,19 +1,29 @@
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
6
8
  end
7
9
 
8
10
  def match?(target_url)
9
11
  case @url
10
12
  when String
11
- @url == target_url || File.fnmatch?(@url, target_url)
13
+ joined_url == target_url || File.fnmatch?(@url, target_url)
12
14
  when Regexp
13
15
  @url.match?(target_url)
14
16
  else
15
17
  false
16
18
  end
17
19
  end
20
+
21
+ private def joined_url
22
+ if @base_url && !@url.start_with?('*')
23
+ URI.join(@base_url, @url).to_s
24
+ else
25
+ @url
26
+ end
27
+ end
18
28
  end
19
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.7.0'
5
- COMPATIBLE_PLAYWRIGHT_VERSION = '1.12.0'
4
+ VERSION = '0.9.0'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.14.0'
6
6
  end
@@ -0,0 +1,164 @@
1
+ require 'openssl'
2
+ require 'socket'
3
+
4
+ begin
5
+ require 'websocket/driver'
6
+ rescue LoadError
7
+ raise "websocket-driver is required. Add `gem 'websocket-driver'` to your Gemfile"
8
+ end
9
+
10
+ # ref: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/client_socket.rb
11
+ # ref: https://github.com/cavalle/chrome_remote/blob/master/lib/chrome_remote/web_socket_client.rb
12
+ module Playwright
13
+ class WebSocketClient
14
+ class SecureSocketFactory
15
+ def initialize(host, port)
16
+ @host = host
17
+ @port = port || 443
18
+ end
19
+
20
+ def create
21
+ tcp_socket = TCPSocket.new(@host, @port)
22
+ OpenSSL::SSL::SSLSocket.new(tcp_socket).tap(&:connect)
23
+ end
24
+ end
25
+
26
+ class DriverImpl # providing #url, #write(string)
27
+ def initialize(url)
28
+ @url = url
29
+
30
+ endpoint = URI.parse(url)
31
+ @socket =
32
+ if endpoint.scheme == 'wss'
33
+ SecureSocketFactory.new(endpoint.host, endpoint.port).create
34
+ else
35
+ TCPSocket.new(endpoint.host, endpoint.port)
36
+ end
37
+ end
38
+
39
+ attr_reader :url
40
+
41
+ def write(data)
42
+ @socket.write(data)
43
+ rescue Errno::EPIPE
44
+ raise EOFError.new('already closed')
45
+ rescue Errno::ECONNRESET
46
+ raise EOFError.new('closed by remote')
47
+ end
48
+
49
+ def readpartial(maxlen = 1024)
50
+ @socket.readpartial(maxlen)
51
+ rescue Errno::ECONNRESET
52
+ raise EOFError.new('closed by remote')
53
+ end
54
+
55
+ def disconnect
56
+ @socket.close
57
+ end
58
+ end
59
+
60
+ STATE_CONNECTING = 0
61
+ STATE_OPENED = 1
62
+ STATE_CLOSING = 2
63
+ STATE_CLOSED = 3
64
+
65
+ def initialize(url:, max_payload_size:)
66
+ @impl = DriverImpl.new(url)
67
+ @driver = ::WebSocket::Driver.client(@impl, max_length: max_payload_size)
68
+
69
+ setup
70
+ end
71
+
72
+ class TransportError < StandardError; end
73
+
74
+ private def setup
75
+ @ready_state = STATE_CONNECTING
76
+ @driver.on(:open) do
77
+ @ready_state = STATE_OPENED
78
+ handle_on_open
79
+ end
80
+ @driver.on(:close) do |event|
81
+ @ready_state = STATE_CLOSED
82
+ handle_on_close(reason: event.reason, code: event.code)
83
+ end
84
+ @driver.on(:error) do |event|
85
+ if !handle_on_error(error_message: event.message)
86
+ raise TransportError.new(event.message)
87
+ end
88
+ end
89
+ @driver.on(:message) do |event|
90
+ handle_on_message(event.data)
91
+ end
92
+ end
93
+
94
+ private def wait_for_data
95
+ @driver.parse(@impl.readpartial)
96
+ end
97
+
98
+ def start
99
+ @driver.start
100
+
101
+ Thread.new do
102
+ wait_for_data until @ready_state >= STATE_CLOSING
103
+ rescue EOFError
104
+ # Google Chrome was gone.
105
+ # We have nothing todo. Just finish polling.
106
+ if @ready_state < STATE_CLOSING
107
+ handle_on_close(reason: 'Going Away', code: 1001)
108
+ end
109
+ end
110
+ end
111
+
112
+ # @param message [String]
113
+ def send_text(message)
114
+ return if @ready_state >= STATE_CLOSING
115
+ @driver.text(message)
116
+ end
117
+
118
+ def close(code: 1000, reason: "")
119
+ return if @ready_state >= STATE_CLOSING
120
+ @ready_state = STATE_CLOSING
121
+ @driver.close(reason, code)
122
+ end
123
+
124
+ def on_open(&block)
125
+ @on_open = block
126
+ end
127
+
128
+ # @param block [Proc(reason: String, code: Numeric)]
129
+ def on_close(&block)
130
+ @on_close = block
131
+ end
132
+
133
+ # @param block [Proc(error_message: String)]
134
+ def on_error(&block)
135
+ @on_error = block
136
+ end
137
+
138
+ def on_message(&block)
139
+ @on_message = block
140
+ end
141
+
142
+ private def handle_on_open
143
+ @on_open&.call
144
+ end
145
+
146
+ private def handle_on_close(reason:, code:)
147
+ @on_close&.call(reason, code)
148
+ @impl.disconnect
149
+ end
150
+
151
+ private def handle_on_error(error_message:)
152
+ return false if @on_error.nil?
153
+
154
+ @on_error.call(error_message)
155
+ true
156
+ end
157
+
158
+ private def handle_on_message(data)
159
+ return if @ready_state != STATE_OPENED
160
+
161
+ @on_message&.call(data)
162
+ end
163
+ end
164
+ end