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.
- checksums.yaml +4 -4
- data/README.md +20 -8
- data/docs/api_coverage.md +123 -73
- data/lib/playwright.rb +48 -9
- data/lib/playwright/channel.rb +12 -2
- data/lib/playwright/channel_owner.rb +3 -5
- data/lib/playwright/channel_owners/android.rb +1 -1
- data/lib/playwright/channel_owners/android_device.rb +11 -11
- data/lib/playwright/channel_owners/artifact.rb +30 -0
- data/lib/playwright/channel_owners/binding_call.rb +3 -0
- data/lib/playwright/channel_owners/browser.rb +22 -1
- data/lib/playwright/channel_owners/browser_context.rb +155 -4
- data/lib/playwright/channel_owners/browser_type.rb +28 -0
- data/lib/playwright/channel_owners/dialog.rb +28 -0
- data/lib/playwright/channel_owners/element_handle.rb +18 -5
- data/lib/playwright/channel_owners/frame.rb +40 -5
- data/lib/playwright/channel_owners/js_handle.rb +3 -3
- data/lib/playwright/channel_owners/page.rb +172 -51
- data/lib/playwright/channel_owners/playwright.rb +24 -27
- data/lib/playwright/channel_owners/request.rb +27 -3
- data/lib/playwright/channel_owners/response.rb +60 -0
- data/lib/playwright/channel_owners/route.rb +78 -0
- data/lib/playwright/channel_owners/selectors.rb +19 -1
- data/lib/playwright/channel_owners/stream.rb +15 -0
- data/lib/playwright/connection.rb +11 -32
- data/lib/playwright/download.rb +27 -0
- data/lib/playwright/errors.rb +6 -0
- data/lib/playwright/events.rb +2 -5
- data/lib/playwright/keyboard_impl.rb +1 -1
- data/lib/playwright/mouse_impl.rb +41 -0
- data/lib/playwright/playwright_api.rb +3 -1
- data/lib/playwright/route_handler_entry.rb +28 -0
- data/lib/playwright/select_option_values.rb +14 -4
- data/lib/playwright/transport.rb +28 -7
- data/lib/playwright/url_matcher.rb +1 -1
- data/lib/playwright/utils.rb +11 -2
- data/lib/playwright/version.rb +1 -1
- data/lib/playwright/video.rb +51 -0
- data/lib/playwright/wait_helper.rb +2 -2
- data/lib/playwright_api/accessibility.rb +39 -1
- data/lib/playwright_api/android.rb +72 -5
- data/lib/playwright_api/android_device.rb +139 -26
- data/lib/playwright_api/android_input.rb +17 -13
- data/lib/playwright_api/android_socket.rb +16 -0
- data/lib/playwright_api/android_web_view.rb +21 -0
- data/lib/playwright_api/browser.rb +87 -19
- data/lib/playwright_api/browser_context.rb +216 -32
- data/lib/playwright_api/browser_type.rb +45 -58
- data/lib/playwright_api/dialog.rb +54 -7
- data/lib/playwright_api/element_handle.rb +113 -33
- data/lib/playwright_api/file_chooser.rb +6 -1
- data/lib/playwright_api/frame.rb +238 -43
- data/lib/playwright_api/js_handle.rb +20 -2
- data/lib/playwright_api/keyboard.rb +48 -1
- data/lib/playwright_api/mouse.rb +26 -5
- data/lib/playwright_api/page.rb +534 -63
- data/lib/playwright_api/playwright.rb +43 -47
- data/lib/playwright_api/request.rb +38 -12
- data/lib/playwright_api/response.rb +27 -10
- data/lib/playwright_api/route.rb +51 -6
- data/lib/playwright_api/selectors.rb +28 -2
- data/lib/playwright_api/touchscreen.rb +1 -1
- data/lib/playwright_api/web_socket.rb +15 -0
- data/lib/playwright_api/worker.rb +25 -1
- data/playwright.gemspec +4 -2
- metadata +42 -14
- data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
- data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
- data/lib/playwright/channel_owners/download.rb +0 -27
- data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
- data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
- data/lib/playwright_api/binding_call.rb +0 -27
- data/lib/playwright_api/chromium_browser_context.rb +0 -59
- data/lib/playwright_api/download.rb +0 -100
- data/lib/playwright_api/video.rb +0 -24
@@ -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?(
|
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(
|
4
|
-
@params =
|
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
|
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
|
data/lib/playwright/transport.rb
CHANGED
@@ -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
|
-
@
|
28
|
-
|
29
|
-
|
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
|
42
|
-
@stdin, @stdout, @stderr, @thread = Open3.popen3(@driver_executable_path
|
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
|
data/lib/playwright/utils.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
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
|
data/lib/playwright/version.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
-
#
|
71
|
+
# Returns the list of detected Android devices.
|
6
72
|
def devices
|
7
73
|
wrap_impl(@impl.devices)
|
8
74
|
end
|
9
75
|
|
10
|
-
#
|
11
|
-
def
|
12
|
-
|
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
|