playwright-ruby-client 0.3.0 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
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