playwright-ruby-client 0.3.0 → 0.5.6

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -5
  3. data/docs/api_coverage.md +14 -25
  4. data/lib/playwright.rb +47 -9
  5. data/lib/playwright/channel.rb +19 -9
  6. data/lib/playwright/channel_owners/artifact.rb +30 -0
  7. data/lib/playwright/channel_owners/browser.rb +21 -0
  8. data/lib/playwright/channel_owners/browser_context.rb +5 -0
  9. data/lib/playwright/channel_owners/browser_type.rb +28 -0
  10. data/lib/playwright/channel_owners/element_handle.rb +7 -7
  11. data/lib/playwright/channel_owners/frame.rb +26 -5
  12. data/lib/playwright/channel_owners/js_handle.rb +2 -2
  13. data/lib/playwright/channel_owners/page.rb +78 -15
  14. data/lib/playwright/channel_owners/playwright.rb +22 -29
  15. data/lib/playwright/channel_owners/stream.rb +15 -0
  16. data/lib/playwright/connection.rb +11 -32
  17. data/lib/playwright/download.rb +27 -0
  18. data/lib/playwright/errors.rb +6 -0
  19. data/lib/playwright/events.rb +2 -5
  20. data/lib/playwright/keyboard_impl.rb +1 -1
  21. data/lib/playwright/mouse_impl.rb +41 -0
  22. data/lib/playwright/playwright_api.rb +5 -3
  23. data/lib/playwright/route_handler_entry.rb +1 -9
  24. data/lib/playwright/select_option_values.rb +31 -22
  25. data/lib/playwright/transport.rb +29 -7
  26. data/lib/playwright/url_matcher.rb +1 -1
  27. data/lib/playwright/utils.rb +8 -0
  28. data/lib/playwright/version.rb +1 -1
  29. data/lib/playwright/video.rb +51 -0
  30. data/lib/playwright/wait_helper.rb +2 -2
  31. data/lib/playwright_api/accessibility.rb +39 -1
  32. data/lib/playwright_api/android.rb +10 -10
  33. data/lib/playwright_api/android_device.rb +10 -9
  34. data/lib/playwright_api/browser.rb +83 -8
  35. data/lib/playwright_api/browser_context.rb +157 -9
  36. data/lib/playwright_api/browser_type.rb +35 -4
  37. data/lib/playwright_api/console_message.rb +6 -6
  38. data/lib/playwright_api/dialog.rb +28 -8
  39. data/lib/playwright_api/element_handle.rb +111 -37
  40. data/lib/playwright_api/file_chooser.rb +5 -0
  41. data/lib/playwright_api/frame.rb +228 -37
  42. data/lib/playwright_api/js_handle.rb +26 -3
  43. data/lib/playwright_api/keyboard.rb +48 -1
  44. data/lib/playwright_api/mouse.rb +26 -5
  45. data/lib/playwright_api/page.rb +454 -46
  46. data/lib/playwright_api/playwright.rb +26 -9
  47. data/lib/playwright_api/request.rb +34 -6
  48. data/lib/playwright_api/response.rb +6 -6
  49. data/lib/playwright_api/route.rb +30 -6
  50. data/lib/playwright_api/selectors.rb +32 -6
  51. data/lib/playwright_api/touchscreen.rb +1 -1
  52. data/lib/playwright_api/worker.rb +25 -1
  53. data/playwright.gemspec +4 -2
  54. metadata +37 -14
  55. data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
  56. data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
  57. data/lib/playwright/channel_owners/download.rb +0 -27
  58. data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
  59. data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
  60. data/lib/playwright_api/binding_call.rb +0 -32
  61. data/lib/playwright_api/chromium_browser_context.rb +0 -59
  62. data/lib/playwright_api/download.rb +0 -95
  63. data/lib/playwright_api/video.rb +0 -24
@@ -10,7 +10,7 @@ module Playwright
10
10
  end
11
11
 
12
12
  def up(key)
13
- @channel.send_message_to_server('keyboardDown', key: key)
13
+ @channel.send_message_to_server('keyboardUp', key: key)
14
14
  end
15
15
 
16
16
  def insert_text(text)
@@ -3,5 +3,46 @@ module Playwright
3
3
  def initialize(channel)
4
4
  @channel = channel
5
5
  end
6
+
7
+ def move(x, y, steps: nil)
8
+ params = { x: x, y: y, steps: steps }.compact
9
+ @channel.send_message_to_server('mouseMove', params)
10
+ nil
11
+ end
12
+
13
+ def down(button: nil, clickCount: nil)
14
+ params = { button: button, clickCount: clickCount }.compact
15
+ @channel.send_message_to_server('mouseDown', params)
16
+ nil
17
+ end
18
+
19
+ def up(button: nil, clickCount: nil)
20
+ params = { button: button, clickCount: clickCount }.compact
21
+ @channel.send_message_to_server('mouseUp', params)
22
+ nil
23
+ end
24
+
25
+ def click(
26
+ x,
27
+ y,
28
+ button: nil,
29
+ clickCount: nil,
30
+ delay: nil)
31
+
32
+ params = {
33
+ x: x,
34
+ y: y,
35
+ button: button,
36
+ clickCount: clickCount,
37
+ delay: delay,
38
+ }.compact
39
+ @channel.send_message_to_server('mouseClick', params)
40
+
41
+ nil
42
+ end
43
+
44
+ def dblclick(x, y, button: nil, delay: nil)
45
+ click(x, y, button: button, clickCount: 2, delay: delay)
46
+ end
6
47
  end
7
48
  end
@@ -14,7 +14,7 @@ module Playwright
14
14
  when ApiImplementation
15
15
  ApiImplementationWrapper.new(channel_owner_or_api_implementation).wrap
16
16
  else
17
- nil
17
+ channel_owner_or_api_implementation
18
18
  end
19
19
  end
20
20
 
@@ -119,12 +119,14 @@ module Playwright
119
119
  if object.is_a?(Array)
120
120
  object.map { |obj| wrap_impl(obj) }
121
121
  else
122
- ::Playwright::PlaywrightApi.wrap(object) || object
122
+ ::Playwright::PlaywrightApi.wrap(object)
123
123
  end
124
124
  end
125
125
 
126
126
  private def unwrap_impl(object)
127
- if object.is_a?(PlaywrightApi)
127
+ if object.is_a?(Array)
128
+ object.map { |obj| unwrap_impl(obj) }
129
+ elsif object.is_a?(PlaywrightApi)
128
130
  object.instance_variable_get(:@impl)
129
131
  else
130
132
  object
@@ -9,7 +9,7 @@ module Playwright
9
9
  end
10
10
 
11
11
  def handle(route, request)
12
- if url_match?(request.url)
12
+ if @url_matcher.match?(request.url)
13
13
  @handler.call(route, request)
14
14
  true
15
15
  else
@@ -24,13 +24,5 @@ module Playwright
24
24
  @url_value == url
25
25
  end
26
26
  end
27
-
28
- private def url_match?(request_url)
29
- if @url_value.is_a?(Regexp)
30
- @url_matcher.match?(request_url)
31
- else
32
- @url_matcher.match?(request_url) || File.fnmatch?(@url_value, request_url)
33
- end
34
- end
35
27
  end
36
28
  end
@@ -1,18 +1,30 @@
1
1
  module Playwright
2
2
  class SelectOptionValues
3
3
  def initialize(element: nil, index: nil, value: nil, label: nil)
4
- @params =
5
- if element
6
- convert(elmeent)
7
- elsif index
8
- convert(index)
9
- elsif value
10
- convert(value)
11
- elsif label
12
- convert(label)
13
- else
14
- {}
15
- end
4
+ params = {}
5
+
6
+ options = []
7
+ if value
8
+ options.concat(convert(:value, value))
9
+ end
10
+
11
+ if index
12
+ options.concat(convert(:index, index))
13
+ end
14
+
15
+ if label
16
+ options.concat(convert(:label, label))
17
+ end
18
+
19
+ unless options.empty?
20
+ params[:options] = options
21
+ end
22
+
23
+ if element
24
+ params[:elements] = convert(:element, element)
25
+ end
26
+
27
+ @params = params
16
28
  end
17
29
 
18
30
  # @return [Hash]
@@ -20,22 +32,19 @@ module Playwright
20
32
  @params
21
33
  end
22
34
 
23
- private def convert(values)
24
- return convert([values]) unless values.is_a?(Enumerable)
25
- return {} if values.empty?
35
+ private def convert(key, values)
36
+ return convert(key, [values]) unless values.is_a?(Enumerable)
37
+ return [] if values.empty?
26
38
  values.each_with_index do |value, index|
27
- unless values
39
+ unless value
28
40
  raise ArgumentError.new("options[#{index}]: expected object, got null")
29
41
  end
30
42
  end
31
43
 
32
- case values.first
33
- when ElementHandle
34
- { elements: values.map(&:channel) }
35
- when String
36
- { options: values.map { |value| { value: value } } }
44
+ if key == :element
45
+ values.map(&:channel)
37
46
  else
38
- { options: values }
47
+ values.map { |value| { key => value } }
39
48
  end
40
49
  end
41
50
  end
@@ -12,39 +12,46 @@ module Playwright
12
12
  def initialize(playwright_cli_executable_path:)
13
13
  @driver_executable_path = playwright_cli_executable_path
14
14
  @debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
15
+ @mutex = Mutex.new
15
16
  end
16
17
 
17
18
  def on_message_received(&block)
18
19
  @on_message = block
19
20
  end
20
21
 
22
+ def on_driver_crashed(&block)
23
+ @on_driver_crashed = block
24
+ end
25
+
21
26
  class AlreadyDisconnectedError < StandardError ; end
22
27
 
23
28
  # @param message [Hash]
24
29
  def send_message(message)
25
30
  debug_send_message(message) if @debug
26
31
  msg = JSON.dump(message)
27
- @stdin.write([msg.size].pack('V')) # unsigned 32bit, little endian
28
- @stdin.write(msg)
29
- rescue Errno::EPIPE
32
+ @mutex.synchronize {
33
+ @stdin.write([msg.bytes.length].pack('V')) # unsigned 32bit, little endian, real byte size instead of chars
34
+ @stdin.write(msg) # write UTF-8 in binary mode as byte stream
35
+ }
36
+ rescue Errno::EPIPE, IOError
30
37
  raise AlreadyDisconnectedError.new('send_message failed')
31
38
  end
32
39
 
33
40
  # Terminate playwright-cli driver.
34
41
  def stop
35
42
  [@stdin, @stdout, @stderr].each { |io| io.close unless io.closed? }
43
+ @thread&.terminate
36
44
  end
37
45
 
38
46
  # Start `playwright-cli run-driver`
39
47
  #
40
48
  # @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')
49
+ def async_run
50
+ @stdin, @stdout, @stderr, @thread = Open3.popen3("#{@driver_executable_path} run-driver")
51
+ @stdin.binmode # Ensure Strings are written 1:1 without encoding conversion, necessary for integer values
43
52
 
44
53
  Thread.new { handle_stdout }
45
54
  Thread.new { handle_stderr }
46
-
47
- @thread.join
48
55
  end
49
56
 
50
57
  private
@@ -69,6 +76,21 @@ module Playwright
69
76
 
70
77
  def handle_stderr
71
78
  while err = @stderr.read
79
+ # sometimed driver crashes with the error below.
80
+ # --------
81
+ # undefined:1
82
+ # �
83
+ # ^
84
+
85
+ # SyntaxError: Unexpected token � in JSON at position 0
86
+ # at JSON.parse (<anonymous>)
87
+ # at Transport.transport.onmessage (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/cli/driver.js:42:73)
88
+ # at Immediate.<anonymous> (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/protocol/transport.js:74:26)
89
+ # at processImmediate (internal/timers.js:461:21)
90
+ if err.include?('undefined:1')
91
+ @on_driver_crashed&.call
92
+ break
93
+ end
72
94
  $stderr.write(err)
73
95
  end
74
96
  rescue IOError
@@ -8,7 +8,7 @@ module Playwright
8
8
  def match?(target_url)
9
9
  case @url
10
10
  when String
11
- @url == target_url
11
+ @url == target_url || File.fnmatch?(@url, target_url)
12
12
  when Regexp
13
13
  @url.match?(target_url)
14
14
  else
@@ -11,6 +11,14 @@ module Playwright
11
11
  if params[:extraHTTPHeaders]
12
12
  params[:extraHTTPHeaders] = ::Playwright::HttpHeaders.new(params[:extraHTTPHeaders]).as_serialized
13
13
  end
14
+ if params[:record_video_dir]
15
+ params[:recordVideo] = {
16
+ dir: params.delete(:record_video_dir)
17
+ }
18
+ if params[:record_video_size]
19
+ params[:recordVideo][:size] = params.delete(:record_video_size)
20
+ end
21
+ end
14
22
  if params[:storageState].is_a?(String)
15
23
  params[:storageState] = JSON.parse(File.read(params[:storageState]))
16
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.6'
5
5
  end
@@ -0,0 +1,51 @@
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
+ wait_for_artifact_and do |artifact|
26
+ artifact.absolute_path
27
+ end
28
+ end
29
+
30
+ def save_as(path)
31
+ wait_for_artifact_and do |artifact|
32
+ artifact.save_as(path)
33
+ end
34
+ end
35
+
36
+ def delete
37
+ wait_for_artifact_and do |artifact|
38
+ artifact.delete
39
+ end
40
+ end
41
+
42
+ private def wait_for_artifact_and(&block)
43
+ artifact = @artifact.value!
44
+ unless artifact
45
+ raise 'Page did not produce any video frames'
46
+ end
47
+
48
+ block.call(artifact)
49
+ end
50
+ end
51
+ end
@@ -22,9 +22,9 @@ module Playwright
22
22
  def reject_on_timeout(timeout_ms, message)
23
23
  return if timeout_ms <= 0
24
24
 
25
- Concurrent::Promises.schedule(timeout_ms / 1000.0) {
25
+ Concurrent::Promises.schedule(timeout_ms / 1000.0) do
26
26
  reject(TimeoutError.new(message: message))
27
- }
27
+ end
28
28
 
29
29
  self
30
30
  end
@@ -6,7 +6,7 @@ module Playwright
6
6
  # Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might
7
7
  # have wildly different output.
8
8
  #
9
- # Rendering engines of Chromium, Firefox and Webkit have a concept of "accessibility tree", which is then translated into
9
+ # Rendering engines of Chromium, Firefox and WebKit have a concept of "accessibility tree", which is then translated into
10
10
  # different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree.
11
11
  #
12
12
  # Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific
@@ -28,6 +28,11 @@ module Playwright
28
28
  # console.log(snapshot);
29
29
  # ```
30
30
  #
31
+ # ```java
32
+ # String snapshot = page.accessibility().snapshot();
33
+ # System.out.println(snapshot);
34
+ # ```
35
+ #
31
36
  # ```python async
32
37
  # snapshot = await page.accessibility.snapshot()
33
38
  # print(snapshot)
@@ -38,6 +43,11 @@ module Playwright
38
43
  # print(snapshot)
39
44
  # ```
40
45
  #
46
+ # ```csharp
47
+ # var accessibilitySnapshot = await Page.Accessibility.SnapshotAsync();
48
+ # Console.WriteLine(accessibilitySnapshot);
49
+ # ```
50
+ #
41
51
  # An example of logging the focused node's name:
42
52
  #
43
53
  #
@@ -57,6 +67,34 @@ module Playwright
57
67
  # }
58
68
  # ```
59
69
  #
70
+ # ```csharp
71
+ # Func<AccessibilitySnapshotResult, AccessibilitySnapshotResult> findFocusedNode = root =>
72
+ # {
73
+ # var nodes = new Stack<AccessibilitySnapshotResult>(new[] { root });
74
+ # while (nodes.Count > 0)
75
+ # {
76
+ # var node = nodes.Pop();
77
+ # if (node.Focused) return node;
78
+ # foreach (var innerNode in node.Children)
79
+ # {
80
+ # nodes.Push(innerNode);
81
+ # }
82
+ # }
83
+ #
84
+ # return null;
85
+ # };
86
+ #
87
+ # var accessibilitySnapshot = await Page.Accessibility.SnapshotAsync();
88
+ # var focusedNode = findFocusedNode(accessibilitySnapshot);
89
+ # if(focusedNode != null)
90
+ # Console.WriteLine(focusedNode.Name);
91
+ # ```
92
+ #
93
+ # ```java
94
+ # // FIXME
95
+ # String snapshot = page.accessibility().snapshot();
96
+ # ```
97
+ #
60
98
  # ```python async
61
99
  # def find_focused_node(node):
62
100
  # if (node.get("focused"))
@@ -3,18 +3,18 @@ module Playwright
3
3
  #
4
4
  #
5
5
  # ```js
6
- # const { _android } = require('playwright');
6
+ # const { _android: android } = require('playwright');
7
7
  # ```
8
8
  #
9
9
  # An example of the Android automation script would be:
10
10
  #
11
11
  #
12
12
  # ```js
13
- # const { _android } = require('playwright');
13
+ # const { _android: android } = require('playwright');
14
14
  #
15
15
  # (async () => {
16
16
  # // Connect to the device.
17
- # const [device] = await playwright._android.devices();
17
+ # const [device] = await android.devices();
18
18
  # console.log(`Model: ${device.model()}`);
19
19
  # console.log(`Serial: ${device.serial()}`);
20
20
  # // Take screenshot of the whole device.
@@ -35,7 +35,7 @@ module Playwright
35
35
  #
36
36
  # // Work with WebView's page as usual.
37
37
  # const page = await webview.page();
38
- # await page.page.waitForNavigation({ url: /.*microsoft\/playwright.*/ });
38
+ # await page.waitForNavigation({ url: /.*microsoft\/playwright.*/ });
39
39
  # console.log(await page.title());
40
40
  # }
41
41
  #
@@ -79,12 +79,6 @@ module Playwright
79
79
  end
80
80
  alias_method :default_timeout=, :set_default_timeout
81
81
 
82
- # -- inherited from EventEmitter --
83
- # @nodoc
84
- def off(event, callback)
85
- event_emitter_proxy.off(event, callback)
86
- end
87
-
88
82
  # -- inherited from EventEmitter --
89
83
  # @nodoc
90
84
  def once(event, callback)
@@ -97,6 +91,12 @@ module Playwright
97
91
  event_emitter_proxy.on(event, callback)
98
92
  end
99
93
 
94
+ # -- inherited from EventEmitter --
95
+ # @nodoc
96
+ def off(event, callback)
97
+ event_emitter_proxy.off(event, callback)
98
+ end
99
+
100
100
  private def event_emitter_proxy
101
101
  @event_emitter_proxy ||= EventEmitterProxy.new(self, @impl)
102
102
  end