playwright-ruby-client 0.1.0 → 0.5.3

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