playwright-ruby-client 0.1.0 → 0.5.3

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -8
  3. data/docs/api_coverage.md +123 -73
  4. data/lib/playwright.rb +48 -9
  5. data/lib/playwright/channel.rb +12 -2
  6. data/lib/playwright/channel_owner.rb +3 -5
  7. data/lib/playwright/channel_owners/android.rb +1 -1
  8. data/lib/playwright/channel_owners/android_device.rb +11 -11
  9. data/lib/playwright/channel_owners/artifact.rb +30 -0
  10. data/lib/playwright/channel_owners/binding_call.rb +3 -0
  11. data/lib/playwright/channel_owners/browser.rb +22 -1
  12. data/lib/playwright/channel_owners/browser_context.rb +155 -4
  13. data/lib/playwright/channel_owners/browser_type.rb +28 -0
  14. data/lib/playwright/channel_owners/dialog.rb +28 -0
  15. data/lib/playwright/channel_owners/element_handle.rb +18 -5
  16. data/lib/playwright/channel_owners/frame.rb +40 -5
  17. data/lib/playwright/channel_owners/js_handle.rb +3 -3
  18. data/lib/playwright/channel_owners/page.rb +172 -51
  19. data/lib/playwright/channel_owners/playwright.rb +24 -27
  20. data/lib/playwright/channel_owners/request.rb +27 -3
  21. data/lib/playwright/channel_owners/response.rb +60 -0
  22. data/lib/playwright/channel_owners/route.rb +78 -0
  23. data/lib/playwright/channel_owners/selectors.rb +19 -1
  24. data/lib/playwright/channel_owners/stream.rb +15 -0
  25. data/lib/playwright/connection.rb +11 -32
  26. data/lib/playwright/download.rb +27 -0
  27. data/lib/playwright/errors.rb +6 -0
  28. data/lib/playwright/events.rb +2 -5
  29. data/lib/playwright/keyboard_impl.rb +1 -1
  30. data/lib/playwright/mouse_impl.rb +41 -0
  31. data/lib/playwright/playwright_api.rb +3 -1
  32. data/lib/playwright/route_handler_entry.rb +28 -0
  33. data/lib/playwright/select_option_values.rb +14 -4
  34. data/lib/playwright/transport.rb +28 -7
  35. data/lib/playwright/url_matcher.rb +1 -1
  36. data/lib/playwright/utils.rb +11 -2
  37. data/lib/playwright/version.rb +1 -1
  38. data/lib/playwright/video.rb +51 -0
  39. data/lib/playwright/wait_helper.rb +2 -2
  40. data/lib/playwright_api/accessibility.rb +39 -1
  41. data/lib/playwright_api/android.rb +72 -5
  42. data/lib/playwright_api/android_device.rb +139 -26
  43. data/lib/playwright_api/android_input.rb +17 -13
  44. data/lib/playwright_api/android_socket.rb +16 -0
  45. data/lib/playwright_api/android_web_view.rb +21 -0
  46. data/lib/playwright_api/browser.rb +87 -19
  47. data/lib/playwright_api/browser_context.rb +216 -32
  48. data/lib/playwright_api/browser_type.rb +45 -58
  49. data/lib/playwright_api/dialog.rb +54 -7
  50. data/lib/playwright_api/element_handle.rb +113 -33
  51. data/lib/playwright_api/file_chooser.rb +6 -1
  52. data/lib/playwright_api/frame.rb +238 -43
  53. data/lib/playwright_api/js_handle.rb +20 -2
  54. data/lib/playwright_api/keyboard.rb +48 -1
  55. data/lib/playwright_api/mouse.rb +26 -5
  56. data/lib/playwright_api/page.rb +534 -63
  57. data/lib/playwright_api/playwright.rb +43 -47
  58. data/lib/playwright_api/request.rb +38 -12
  59. data/lib/playwright_api/response.rb +27 -10
  60. data/lib/playwright_api/route.rb +51 -6
  61. data/lib/playwright_api/selectors.rb +28 -2
  62. data/lib/playwright_api/touchscreen.rb +1 -1
  63. data/lib/playwright_api/web_socket.rb +15 -0
  64. data/lib/playwright_api/worker.rb +25 -1
  65. data/playwright.gemspec +4 -2
  66. metadata +42 -14
  67. data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
  68. data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
  69. data/lib/playwright/channel_owners/download.rb +0 -27
  70. data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
  71. data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
  72. data/lib/playwright_api/binding_call.rb +0 -27
  73. data/lib/playwright_api/chromium_browser_context.rb +0 -59
  74. data/lib/playwright_api/download.rb +0 -100
  75. 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
@@ -124,7 +124,9 @@ module Playwright
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
@@ -0,0 +1,28 @@
1
+ module Playwright
2
+ class RouteHandlerEntry
3
+ # @param url [String]
4
+ # @param handler [Proc]
5
+ def initialize(url, handler)
6
+ @url_value = url
7
+ @url_matcher = UrlMatcher.new(url)
8
+ @handler = handler
9
+ end
10
+
11
+ def handle(route, request)
12
+ if @url_matcher.match?(request.url)
13
+ @handler.call(route, request)
14
+ true
15
+ else
16
+ false
17
+ end
18
+ end
19
+
20
+ def same_value?(url:, handler: nil)
21
+ if handler
22
+ @url_value == url && @handler == handler
23
+ else
24
+ @url_value == url
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,18 @@
1
1
  module Playwright
2
2
  class SelectOptionValues
3
- def initialize(values)
4
- @params = convert(values)
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
5
16
  end
6
17
 
7
18
  # @return [Hash]
@@ -10,8 +21,7 @@ module Playwright
10
21
  end
11
22
 
12
23
  private def convert(values)
13
- return {} unless values
14
- return convert([values]) unless values.is_a?('Array')
24
+ return convert([values]) unless values.is_a?(Enumerable)
15
25
  return {} if values.empty?
16
26
  values.each_with_index do |value, index|
17
27
  unless values
@@ -12,39 +12,45 @@ 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.size].pack('V')) # unsigned 32bit, little endian
34
+ @stdin.write(msg)
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")
43
51
 
44
52
  Thread.new { handle_stdout }
45
53
  Thread.new { handle_stderr }
46
-
47
- @thread.join
48
54
  end
49
55
 
50
56
  private
@@ -69,6 +75,21 @@ module Playwright
69
75
 
70
76
  def handle_stderr
71
77
  while err = @stderr.read
78
+ # sometimed driver crashes with the error below.
79
+ # --------
80
+ # undefined:1
81
+ # �
82
+ # ^
83
+
84
+ # SyntaxError: Unexpected token � in JSON at position 0
85
+ # at JSON.parse (<anonymous>)
86
+ # at Transport.transport.onmessage (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/cli/driver.js:42:73)
87
+ # at Immediate.<anonymous> (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/protocol/transport.js:74:26)
88
+ # at processImmediate (internal/timers.js:461:21)
89
+ if err.include?('undefined:1')
90
+ @on_driver_crashed&.call
91
+ break
92
+ end
72
93
  $stderr.write(err)
73
94
  end
74
95
  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
@@ -3,13 +3,22 @@ module Playwright
3
3
  module PrepareBrowserContextOptions
4
4
  # @see https://github.com/microsoft/playwright/blob/5a2cfdbd47ed3c3deff77bb73e5fac34241f649d/src/client/browserContext.ts#L265
5
5
  private def prepare_browser_context_options(params)
6
- if params[:viewport] == 0
7
- params.delete(:viewport)
6
+ params[:sdkLanguage] = 'ruby'
7
+ if params[:noViewport] == 0
8
+ params.delete(:noViewport)
8
9
  params[:noDefaultViewport] = true
9
10
  end
10
11
  if params[:extraHTTPHeaders]
11
12
  params[:extraHTTPHeaders] = ::Playwright::HttpHeaders.new(params[:extraHTTPHeaders]).as_serialized
12
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
13
22
  if params[:storageState].is_a?(String)
14
23
  params[:storageState] = JSON.parse(File.read(params[:storageState]))
15
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.1.0'
4
+ VERSION = '0.5.3'
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"))
@@ -1,16 +1,83 @@
1
1
  module Playwright
2
- # @nodoc
2
+ # Playwright has **experimental** support for Android automation. You can access android namespace via:
3
+ #
4
+ #
5
+ # ```js
6
+ # const { _android: android } = require('playwright');
7
+ # ```
8
+ #
9
+ # An example of the Android automation script would be:
10
+ #
11
+ #
12
+ # ```js
13
+ # const { _android: android } = require('playwright');
14
+ #
15
+ # (async () => {
16
+ # // Connect to the device.
17
+ # const [device] = await android.devices();
18
+ # console.log(`Model: ${device.model()}`);
19
+ # console.log(`Serial: ${device.serial()}`);
20
+ # // Take screenshot of the whole device.
21
+ # await device.screenshot({ path: 'device.png' });
22
+ #
23
+ # {
24
+ # // --------------------- WebView -----------------------
25
+ #
26
+ # // Launch an application with WebView.
27
+ # await device.shell('am force-stop org.chromium.webview_shell');
28
+ # await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
29
+ # // Get the WebView.
30
+ # const webview = await device.webView({ pkg: 'org.chromium.webview_shell' });
31
+ #
32
+ # // Fill the input box.
33
+ # await device.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'github.com/microsoft/playwright');
34
+ # await device.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter');
35
+ #
36
+ # // Work with WebView's page as usual.
37
+ # const page = await webview.page();
38
+ # await page.waitForNavigation({ url: /.*microsoft\/playwright.*/ });
39
+ # console.log(await page.title());
40
+ # }
41
+ #
42
+ # {
43
+ # // --------------------- Browser -----------------------
44
+ #
45
+ # // Launch Chrome browser.
46
+ # await device.shell('am force-stop com.android.chrome');
47
+ # const context = await device.launchBrowser();
48
+ #
49
+ # // Use BrowserContext as usual.
50
+ # const page = await context.newPage();
51
+ # await page.goto('https://webkit.org/');
52
+ # console.log(await page.evaluate(() => window.location.href));
53
+ # await page.screenshot({ path: 'page.png' });
54
+ #
55
+ # await context.close();
56
+ # }
57
+ #
58
+ # // Close the device.
59
+ # await device.close();
60
+ # })();
61
+ # ```
62
+ #
63
+ # Note that since you don't need Playwright to install web browsers when testing Android, you can omit browser download
64
+ # via setting the following environment variable when installing Playwright:
65
+ #
66
+ # ```sh js
67
+ # $ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright
68
+ # ```
3
69
  class Android < PlaywrightApi
4
70
 
5
- # @nodoc
71
+ # Returns the list of detected Android devices.
6
72
  def devices
7
73
  wrap_impl(@impl.devices)
8
74
  end
9
75
 
10
- # @nodoc
11
- def after_initialize
12
- wrap_impl(@impl.after_initialize)
76
+ # This setting will change the default maximum time for all the methods accepting `timeout` option.
77
+ def set_default_timeout(timeout)
78
+ raise NotImplementedError.new('set_default_timeout is not implemented yet.')
13
79
  end
80
+ alias_method :default_timeout=, :set_default_timeout
14
81
 
15
82
  # -- inherited from EventEmitter --
16
83
  # @nodoc