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.
- checksums.yaml +4 -4
- data/README.md +17 -5
- data/docs/api_coverage.md +14 -25
- data/lib/playwright.rb +47 -9
- data/lib/playwright/channel.rb +19 -9
- data/lib/playwright/channel_owners/artifact.rb +30 -0
- data/lib/playwright/channel_owners/browser.rb +21 -0
- data/lib/playwright/channel_owners/browser_context.rb +5 -0
- data/lib/playwright/channel_owners/browser_type.rb +28 -0
- data/lib/playwright/channel_owners/element_handle.rb +7 -7
- data/lib/playwright/channel_owners/frame.rb +26 -5
- data/lib/playwright/channel_owners/js_handle.rb +2 -2
- data/lib/playwright/channel_owners/page.rb +78 -15
- data/lib/playwright/channel_owners/playwright.rb +22 -29
- 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 +5 -3
- data/lib/playwright/route_handler_entry.rb +1 -9
- data/lib/playwright/select_option_values.rb +31 -22
- data/lib/playwright/transport.rb +29 -7
- data/lib/playwright/url_matcher.rb +1 -1
- data/lib/playwright/utils.rb +8 -0
- 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 +10 -10
- data/lib/playwright_api/android_device.rb +10 -9
- data/lib/playwright_api/browser.rb +83 -8
- data/lib/playwright_api/browser_context.rb +157 -9
- data/lib/playwright_api/browser_type.rb +35 -4
- data/lib/playwright_api/console_message.rb +6 -6
- data/lib/playwright_api/dialog.rb +28 -8
- data/lib/playwright_api/element_handle.rb +111 -37
- data/lib/playwright_api/file_chooser.rb +5 -0
- data/lib/playwright_api/frame.rb +228 -37
- data/lib/playwright_api/js_handle.rb +26 -3
- data/lib/playwright_api/keyboard.rb +48 -1
- data/lib/playwright_api/mouse.rb +26 -5
- data/lib/playwright_api/page.rb +454 -46
- data/lib/playwright_api/playwright.rb +26 -9
- data/lib/playwright_api/request.rb +34 -6
- data/lib/playwright_api/response.rb +6 -6
- data/lib/playwright_api/route.rb +30 -6
- data/lib/playwright_api/selectors.rb +32 -6
- data/lib/playwright_api/touchscreen.rb +1 -1
- data/lib/playwright_api/worker.rb +25 -1
- data/playwright.gemspec +4 -2
- metadata +37 -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 -32
- data/lib/playwright_api/chromium_browser_context.rb +0 -59
- data/lib/playwright_api/download.rb +0 -95
- 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
|
@@ -14,7 +14,7 @@ module Playwright
|
|
14
14
|
when ApiImplementation
|
15
15
|
ApiImplementationWrapper.new(channel_owner_or_api_implementation).wrap
|
16
16
|
else
|
17
|
-
|
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)
|
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?(
|
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
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
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
|
39
|
+
unless value
|
28
40
|
raise ArgumentError.new("options[#{index}]: expected object, got null")
|
29
41
|
end
|
30
42
|
end
|
31
43
|
|
32
|
-
|
33
|
-
|
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
|
-
{
|
47
|
+
values.map { |value| { key => value } }
|
39
48
|
end
|
40
49
|
end
|
41
50
|
end
|
data/lib/playwright/transport.rb
CHANGED
@@ -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
|
-
@
|
28
|
-
|
29
|
-
|
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
|
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")
|
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
|
data/lib/playwright/utils.rb
CHANGED
@@ -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
|
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"))
|
@@ -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
|
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.
|
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
|