playwright-ruby-client 0.0.8 → 1.58.1.alpha1
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.
- checksums.yaml +4 -4
- data/AGENTS.md +4 -0
- data/CLAUDE/api_generation.md +28 -0
- data/CLAUDE/ci_expectations.md +23 -0
- data/CLAUDE/gem_release_flow.md +39 -0
- data/CLAUDE/past_upgrade_pr_patterns.md +42 -0
- data/CLAUDE/playwright_upgrade_workflow.md +35 -0
- data/CLAUDE/rspec_debugging.md +30 -0
- data/CLAUDE/unimplemented_examples.md +18 -0
- data/CLAUDE.md +32 -0
- data/CONTRIBUTING.md +5 -0
- data/README.md +60 -16
- data/documentation/README.md +33 -0
- data/documentation/babel.config.js +3 -0
- data/documentation/docs/api/api_request.md +7 -0
- data/documentation/docs/api/api_request_context.md +298 -0
- data/documentation/docs/api/api_response.md +114 -0
- data/documentation/docs/api/browser.md +237 -0
- data/documentation/docs/api/browser_context.md +503 -0
- data/documentation/docs/api/browser_type.md +184 -0
- data/documentation/docs/api/cdp_session.md +44 -0
- data/documentation/docs/api/clock.md +154 -0
- data/documentation/docs/api/console_message.md +85 -0
- data/documentation/docs/api/dialog.md +84 -0
- data/documentation/docs/api/download.md +111 -0
- data/documentation/docs/api/element_handle.md +694 -0
- data/documentation/docs/api/experimental/_category_.yml +3 -0
- data/documentation/docs/api/experimental/android.md +42 -0
- data/documentation/docs/api/experimental/android_device.md +109 -0
- data/documentation/docs/api/experimental/android_input.md +43 -0
- data/documentation/docs/api/experimental/android_socket.md +7 -0
- data/documentation/docs/api/experimental/android_web_view.md +7 -0
- data/documentation/docs/api/file_chooser.md +53 -0
- data/documentation/docs/api/frame.md +1218 -0
- data/documentation/docs/api/frame_locator.md +348 -0
- data/documentation/docs/api/js_handle.md +121 -0
- data/documentation/docs/api/keyboard.md +170 -0
- data/documentation/docs/api/locator.md +1495 -0
- data/documentation/docs/api/locator_assertions.md +827 -0
- data/documentation/docs/api/mouse.md +86 -0
- data/documentation/docs/api/page.md +1946 -0
- data/documentation/docs/api/page_assertions.md +65 -0
- data/documentation/docs/api/playwright.md +66 -0
- data/documentation/docs/api/request.md +255 -0
- data/documentation/docs/api/response.md +176 -0
- data/documentation/docs/api/route.md +205 -0
- data/documentation/docs/api/selectors.md +63 -0
- data/documentation/docs/api/touchscreen.md +22 -0
- data/documentation/docs/api/tracing.md +129 -0
- data/documentation/docs/api/web_socket.md +51 -0
- data/documentation/docs/api/worker.md +83 -0
- data/documentation/docs/article/api_coverage.mdx +11 -0
- data/documentation/docs/article/getting_started.md +161 -0
- data/documentation/docs/article/guides/_category_.yml +3 -0
- data/documentation/docs/article/guides/download_playwright_driver.md +55 -0
- data/documentation/docs/article/guides/inspector.md +31 -0
- data/documentation/docs/article/guides/launch_browser.md +121 -0
- data/documentation/docs/article/guides/playwright_on_alpine_linux.md +112 -0
- data/documentation/docs/article/guides/rails_integration.md +278 -0
- data/documentation/docs/article/guides/rails_integration_with_null_driver.md +145 -0
- data/documentation/docs/article/guides/recording_video.md +79 -0
- data/documentation/docs/article/guides/rspec_integration.md +59 -0
- data/documentation/docs/article/guides/semi_automation.md +71 -0
- data/documentation/docs/article/guides/use_storage_state.md +78 -0
- data/documentation/docs/include/api_coverage.md +671 -0
- data/documentation/docusaurus.config.js +114 -0
- data/documentation/package.json +39 -0
- data/documentation/sidebars.js +15 -0
- data/documentation/src/components/HomepageFeatures.js +61 -0
- data/documentation/src/components/HomepageFeatures.module.css +13 -0
- data/documentation/src/css/custom.css +44 -0
- data/documentation/src/pages/index.js +49 -0
- data/documentation/src/pages/index.module.css +41 -0
- data/documentation/src/pages/markdown-page.md +7 -0
- data/documentation/static/.nojekyll +0 -0
- data/documentation/static/img/playwright-logo.svg +9 -0
- data/documentation/static/img/playwright-ruby-client.png +0 -0
- data/documentation/static/img/undraw_dropdown_menu.svg +1 -0
- data/documentation/static/img/undraw_web_development.svg +1 -0
- data/documentation/static/img/undraw_windows.svg +1 -0
- data/documentation/yarn.lock +9005 -0
- data/lib/playwright/{input_types/android_input.rb → android_input_impl.rb} +5 -1
- data/lib/playwright/api_implementation.rb +18 -0
- data/lib/playwright/api_response_impl.rb +77 -0
- data/lib/playwright/channel.rb +62 -1
- data/lib/playwright/channel_owner.rb +70 -7
- data/lib/playwright/channel_owners/android.rb +16 -3
- data/lib/playwright/channel_owners/android_device.rb +22 -66
- data/lib/playwright/channel_owners/api_request_context.rb +247 -0
- data/lib/playwright/channel_owners/artifact.rb +40 -0
- data/lib/playwright/channel_owners/binding_call.rb +70 -0
- data/lib/playwright/channel_owners/browser.rb +114 -22
- data/lib/playwright/channel_owners/browser_context.rb +589 -15
- data/lib/playwright/channel_owners/browser_type.rb +90 -1
- data/lib/playwright/channel_owners/cdp_session.rb +19 -0
- data/lib/playwright/channel_owners/dialog.rb +32 -0
- data/lib/playwright/channel_owners/element_handle.rb +107 -43
- data/lib/playwright/channel_owners/fetch_request.rb +8 -0
- data/lib/playwright/channel_owners/frame.rb +334 -104
- data/lib/playwright/channel_owners/js_handle.rb +9 -13
- data/lib/playwright/channel_owners/local_utils.rb +82 -0
- data/lib/playwright/channel_owners/page.rb +778 -95
- data/lib/playwright/channel_owners/playwright.rb +25 -30
- data/lib/playwright/channel_owners/request.rb +120 -18
- data/lib/playwright/channel_owners/response.rb +113 -0
- data/lib/playwright/channel_owners/route.rb +181 -0
- data/lib/playwright/channel_owners/stream.rb +30 -0
- data/lib/playwright/channel_owners/tracing.rb +117 -0
- data/lib/playwright/channel_owners/web_socket.rb +96 -0
- data/lib/playwright/channel_owners/worker.rb +46 -0
- data/lib/playwright/channel_owners/writable_stream.rb +14 -0
- data/lib/playwright/clock_impl.rb +67 -0
- data/lib/playwright/connection.rb +111 -63
- data/lib/playwright/console_message_impl.rb +29 -0
- data/lib/playwright/download_impl.rb +32 -0
- data/lib/playwright/errors.rb +42 -5
- data/lib/playwright/event_emitter.rb +17 -3
- data/lib/playwright/event_emitter_proxy.rb +49 -0
- data/lib/playwright/events.rb +10 -5
- data/lib/playwright/file_chooser_impl.rb +24 -0
- data/lib/playwright/frame_locator_impl.rb +66 -0
- data/lib/playwright/har_router.rb +89 -0
- data/lib/playwright/http_headers.rb +14 -0
- data/lib/playwright/input_files.rb +102 -15
- data/lib/playwright/javascript/expression.rb +7 -11
- data/lib/playwright/javascript/regex.rb +23 -0
- data/lib/playwright/javascript/source_url.rb +16 -0
- data/lib/playwright/javascript/value_parser.rb +108 -19
- data/lib/playwright/javascript/value_serializer.rb +47 -8
- data/lib/playwright/javascript/visitor_info.rb +26 -0
- data/lib/playwright/javascript.rb +2 -10
- data/lib/playwright/{input_types/keyboard.rb → keyboard_impl.rb} +6 -2
- data/lib/playwright/locator_assertions_impl.rb +571 -0
- data/lib/playwright/locator_impl.rb +544 -0
- data/lib/playwright/locator_utils.rb +136 -0
- data/lib/playwright/mouse_impl.rb +57 -0
- data/lib/playwright/page_assertions_impl.rb +154 -0
- data/lib/playwright/playwright_api.rb +102 -30
- data/lib/playwright/raw_headers.rb +61 -0
- data/lib/playwright/route_handler.rb +78 -0
- data/lib/playwright/select_option_values.rb +34 -13
- data/lib/playwright/selectors_impl.rb +45 -0
- data/lib/playwright/test.rb +102 -0
- data/lib/playwright/timeout_settings.rb +9 -4
- data/lib/playwright/touchscreen_impl.rb +14 -0
- data/lib/playwright/transport.rb +61 -10
- data/lib/playwright/url_matcher.rb +24 -2
- data/lib/playwright/utils.rb +48 -13
- data/lib/playwright/version.rb +2 -1
- data/lib/playwright/video.rb +54 -0
- data/lib/playwright/waiter.rb +166 -0
- data/lib/playwright/web_socket_client.rb +167 -0
- data/lib/playwright/web_socket_transport.rb +116 -0
- data/lib/playwright.rb +188 -11
- data/lib/playwright_api/android.rb +46 -11
- data/lib/playwright_api/android_device.rb +182 -31
- data/lib/playwright_api/android_input.rb +22 -13
- data/lib/playwright_api/android_socket.rb +18 -0
- data/lib/playwright_api/android_web_view.rb +24 -0
- data/lib/playwright_api/api_request.rb +26 -0
- data/lib/playwright_api/api_request_context.rb +311 -0
- data/lib/playwright_api/api_response.rb +92 -0
- data/lib/playwright_api/browser.rb +116 -103
- data/lib/playwright_api/browser_context.rb +290 -389
- data/lib/playwright_api/browser_type.rb +96 -118
- data/lib/playwright_api/cdp_session.rb +36 -39
- data/lib/playwright_api/clock.rb +121 -0
- data/lib/playwright_api/console_message.rb +35 -19
- data/lib/playwright_api/dialog.rb +53 -50
- data/lib/playwright_api/download.rb +49 -43
- data/lib/playwright_api/element_handle.rb +354 -402
- data/lib/playwright_api/file_chooser.rb +15 -18
- data/lib/playwright_api/frame.rb +703 -603
- data/lib/playwright_api/frame_locator.rb +285 -0
- data/lib/playwright_api/js_handle.rb +50 -76
- data/lib/playwright_api/keyboard.rb +67 -146
- data/lib/playwright_api/locator.rb +1304 -0
- data/lib/playwright_api/locator_assertions.rb +704 -0
- data/lib/playwright_api/mouse.rb +23 -29
- data/lib/playwright_api/page.rb +1196 -1176
- data/lib/playwright_api/page_assertions.rb +60 -0
- data/lib/playwright_api/playwright.rb +54 -122
- data/lib/playwright_api/request.rb +112 -74
- data/lib/playwright_api/response.rb +92 -20
- data/lib/playwright_api/route.rb +152 -62
- data/lib/playwright_api/selectors.rb +47 -61
- data/lib/playwright_api/touchscreen.rb +8 -2
- data/lib/playwright_api/tracing.rb +128 -0
- data/lib/playwright_api/web_socket.rb +43 -5
- data/lib/playwright_api/worker.rb +74 -34
- data/playwright.gemspec +14 -9
- data/sig/playwright.rbs +658 -0
- metadata +216 -50
- data/docs/api_coverage.md +0 -354
- 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/console_message.rb +0 -21
- data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
- data/lib/playwright/channel_owners/selectors.rb +0 -4
- data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
- data/lib/playwright/input_type.rb +0 -19
- data/lib/playwright/input_types/mouse.rb +0 -4
- data/lib/playwright/input_types/touchscreen.rb +0 -4
- data/lib/playwright/javascript/function.rb +0 -67
- data/lib/playwright/wait_helper.rb +0 -73
- data/lib/playwright_api/accessibility.rb +0 -93
- data/lib/playwright_api/binding_call.rb +0 -23
- data/lib/playwright_api/chromium_browser_context.rb +0 -57
- data/lib/playwright_api/video.rb +0 -24
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module Playwright
|
|
2
|
+
# this module is responsible for running playwright assertions and integrating
|
|
3
|
+
# with test frameworks.
|
|
4
|
+
module Test
|
|
5
|
+
@@expect_timeout = nil
|
|
6
|
+
|
|
7
|
+
def self.expect_timeout
|
|
8
|
+
@@expect_timeout || 5000 # default timeout is 5000ms
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.expect_timeout=(timeout)
|
|
12
|
+
@@expect_timeout = timeout
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.with_timeout(expect_timeout, &block)
|
|
16
|
+
old_timeout = @@expect_timeout
|
|
17
|
+
@@expect_timeout = expect_timeout
|
|
18
|
+
block.call
|
|
19
|
+
ensure
|
|
20
|
+
@@expect_timeout = old_timeout
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# ref: https://github.com/microsoft/playwright-python/blob/main/playwright/sync_api/__init__.py#L90
|
|
24
|
+
class Expect
|
|
25
|
+
def initialize
|
|
26
|
+
@default_expect_timeout = ::Playwright::Test.expect_timeout
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(actual, is_not)
|
|
30
|
+
case actual
|
|
31
|
+
when Page
|
|
32
|
+
PageAssertions.new(
|
|
33
|
+
PageAssertionsImpl.new(
|
|
34
|
+
actual,
|
|
35
|
+
@default_expect_timeout,
|
|
36
|
+
is_not,
|
|
37
|
+
nil,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
when Locator
|
|
41
|
+
LocatorAssertions.new(
|
|
42
|
+
LocatorAssertionsImpl.new(
|
|
43
|
+
actual,
|
|
44
|
+
@default_expect_timeout,
|
|
45
|
+
is_not,
|
|
46
|
+
nil,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
else
|
|
50
|
+
raise NotImplementedError.new("Only locator assertions are currently implemented")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module Matchers
|
|
56
|
+
class PlaywrightMatcher
|
|
57
|
+
def initialize(expectation_method, *args, **kwargs)
|
|
58
|
+
@method = expectation_method
|
|
59
|
+
@args = args
|
|
60
|
+
@kwargs = kwargs
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def matches?(actual)
|
|
64
|
+
Expect.new.call(actual, false).send(@method, *@args, **@kwargs)
|
|
65
|
+
true
|
|
66
|
+
rescue AssertionError => e
|
|
67
|
+
@failure_message = e.full_message
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def does_not_match?(actual)
|
|
72
|
+
Expect.new.call(actual, true).send(@method, *@args, **@kwargs)
|
|
73
|
+
true
|
|
74
|
+
rescue AssertionError => e
|
|
75
|
+
@failure_message = e.full_message
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def failure_message
|
|
80
|
+
@failure_message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def failure_message_when_negated
|
|
84
|
+
@failure_message
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
ALL_ASSERTIONS = PageAssertions.instance_methods(false) + LocatorAssertions.instance_methods(false)
|
|
90
|
+
|
|
91
|
+
ALL_ASSERTIONS
|
|
92
|
+
.map(&:to_s)
|
|
93
|
+
.each do |method_name|
|
|
94
|
+
# to_be_visible => be_visible
|
|
95
|
+
# not_to_be_visible => not_be_visible
|
|
96
|
+
root_method_name = method_name.gsub("to_", "")
|
|
97
|
+
Matchers.send(:define_method, root_method_name) do |*args, **kwargs|
|
|
98
|
+
Matchers::PlaywrightMatcher.new(method_name, *args, **kwargs)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Playwright
|
|
2
2
|
class TimeoutSettings
|
|
3
3
|
DEFAULT_TIMEOUT = 30000
|
|
4
|
+
DEFAULT_LAUNCH_TIMEOUT = 180000 # 3 minutes
|
|
4
5
|
|
|
5
6
|
def initialize(parent = nil)
|
|
6
7
|
@parent = parent
|
|
@@ -8,12 +9,16 @@ module Playwright
|
|
|
8
9
|
|
|
9
10
|
attr_writer :default_timeout, :default_navigation_timeout
|
|
10
11
|
|
|
11
|
-
def navigation_timeout
|
|
12
|
-
@default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
|
|
12
|
+
def navigation_timeout(timeout_override = nil)
|
|
13
|
+
timeout_override || @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
def timeout
|
|
16
|
-
@default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
|
|
16
|
+
def timeout(timeout_override = nil)
|
|
17
|
+
timeout_override || @default_timeout || @parent&.timeout || DEFAULT_TIMEOUT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def launch_timeout(timeout_override = nil)
|
|
21
|
+
timeout_override || @default_timeout || @parent&.launch_timeout || DEFAULT_LAUNCH_TIMEOUT
|
|
17
22
|
end
|
|
18
23
|
end
|
|
19
24
|
end
|
data/lib/playwright/transport.rb
CHANGED
|
@@ -8,47 +8,69 @@ module Playwright
|
|
|
8
8
|
# ref: https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_transport.py
|
|
9
9
|
class Transport
|
|
10
10
|
# @param playwright_cli_executable_path [String] path to playwright-cli.
|
|
11
|
-
# @param debug [Boolean]
|
|
12
11
|
def initialize(playwright_cli_executable_path:)
|
|
13
12
|
@driver_executable_path = playwright_cli_executable_path
|
|
14
13
|
@debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
|
|
14
|
+
@mutex = Mutex.new
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def on_message_received(&block)
|
|
18
18
|
@on_message = block
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def on_driver_closed(&block)
|
|
22
|
+
@on_driver_closed = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_driver_crashed(&block)
|
|
26
|
+
@on_driver_crashed = block
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
class AlreadyDisconnectedError < StandardError ; end
|
|
22
30
|
|
|
23
31
|
# @param message [Hash]
|
|
24
32
|
def send_message(message)
|
|
25
33
|
debug_send_message(message) if @debug
|
|
26
34
|
msg = JSON.dump(message)
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
@mutex.synchronize {
|
|
36
|
+
@stdin.write([msg.bytes.length].pack('V')) # unsigned 32bit, little endian, real byte size instead of chars
|
|
37
|
+
@stdin.write(msg) # write UTF-8 in binary mode as byte stream
|
|
38
|
+
}
|
|
39
|
+
rescue Errno::EPIPE, IOError
|
|
30
40
|
raise AlreadyDisconnectedError.new('send_message failed')
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
# Terminate playwright-cli driver.
|
|
34
44
|
def stop
|
|
35
45
|
[@stdin, @stdout, @stderr].each { |io| io.close unless io.closed? }
|
|
46
|
+
@thread&.join
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
# Start `playwright-cli run-driver`
|
|
39
50
|
#
|
|
40
51
|
# @note This method blocks until playwright-cli exited. Consider using Thread or Future.
|
|
41
|
-
def
|
|
42
|
-
@stdin, @stdout, @stderr, @thread =
|
|
52
|
+
def async_run
|
|
53
|
+
@stdin, @stdout, @stderr, @thread = run_driver_with_open3
|
|
54
|
+
@stdin.binmode # Ensure Strings are written 1:1 without encoding conversion, necessary for integer values
|
|
43
55
|
|
|
44
56
|
Thread.new { handle_stdout }
|
|
45
57
|
Thread.new { handle_stderr }
|
|
46
|
-
|
|
47
|
-
@thread.join
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
private
|
|
51
61
|
|
|
62
|
+
def run_driver_with_open3
|
|
63
|
+
Open3.popen3("#{@driver_executable_path} run-driver", { pgroup: true })
|
|
64
|
+
rescue ArgumentError => err
|
|
65
|
+
# Windows doesn't accept pgroup parameter.
|
|
66
|
+
# ArgumentError: wrong exec option symbol: pgroup
|
|
67
|
+
if err.message =~ /pgroup/
|
|
68
|
+
Open3.popen3("#{@driver_executable_path} run-driver")
|
|
69
|
+
else
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def handle_stdout(packet_size: 32_768)
|
|
53
75
|
while chunk = @stdout.read(4)
|
|
54
76
|
length = chunk.unpack1('V') # unsigned 32bit, little endian
|
|
@@ -65,22 +87,51 @@ module Playwright
|
|
|
65
87
|
end
|
|
66
88
|
rescue IOError
|
|
67
89
|
# disconnected by remote.
|
|
90
|
+
@on_driver_closed&.call
|
|
68
91
|
end
|
|
69
92
|
|
|
70
93
|
def handle_stderr
|
|
71
94
|
while err = @stderr.read
|
|
95
|
+
# sometimed driver crashes with the error below.
|
|
96
|
+
# --------
|
|
97
|
+
# undefined:1
|
|
98
|
+
# �
|
|
99
|
+
# ^
|
|
100
|
+
|
|
101
|
+
# SyntaxError: Unexpected token � in JSON at position 0
|
|
102
|
+
# at JSON.parse (<anonymous>)
|
|
103
|
+
# at Transport.transport.onmessage (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/cli/driver.js:42:73)
|
|
104
|
+
# at Immediate.<anonymous> (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/protocol/transport.js:74:26)
|
|
105
|
+
# at processImmediate (internal/timers.js:461:21)
|
|
106
|
+
if err.include?('undefined:1')
|
|
107
|
+
@on_driver_crashed&.call
|
|
108
|
+
break
|
|
109
|
+
end
|
|
72
110
|
$stderr.write(err)
|
|
73
111
|
end
|
|
74
112
|
rescue IOError
|
|
75
113
|
# disconnected by remote.
|
|
114
|
+
@on_driver_closed&.call
|
|
76
115
|
end
|
|
77
116
|
|
|
78
117
|
def debug_send_message(message)
|
|
79
|
-
|
|
118
|
+
metadata = message.delete(:metadata)
|
|
119
|
+
puts "\x1b[33mSEND>\x1b[0m#{shorten_double_quoted_string(message)}"
|
|
120
|
+
message[:metadata] = metadata
|
|
80
121
|
end
|
|
81
122
|
|
|
82
123
|
def debug_recv_message(message)
|
|
83
|
-
puts "\x1b[33mRECV>\x1b[0m#{message}"
|
|
124
|
+
puts "\x1b[33mRECV>\x1b[0m#{shorten_double_quoted_string(message)}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def shorten_double_quoted_string(message, maxlen: 512)
|
|
128
|
+
message.to_s.gsub(/"([^"]+)"/) do |str|
|
|
129
|
+
if $1.length > maxlen
|
|
130
|
+
"\"#{$1[0...maxlen]}...\""
|
|
131
|
+
else
|
|
132
|
+
str
|
|
133
|
+
end
|
|
134
|
+
end
|
|
84
135
|
end
|
|
85
136
|
end
|
|
86
137
|
end
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
module Playwright
|
|
2
2
|
class UrlMatcher
|
|
3
3
|
# @param url [String|Regexp]
|
|
4
|
-
|
|
4
|
+
# @param base_url [String|nil]
|
|
5
|
+
def initialize(url, base_url:)
|
|
5
6
|
@url = url
|
|
7
|
+
@base_url = base_url
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def as_pattern
|
|
11
|
+
case @url
|
|
12
|
+
when String
|
|
13
|
+
{ glob: @url }
|
|
14
|
+
when Regexp
|
|
15
|
+
regex = JavaScript::Regex.new(@url)
|
|
16
|
+
{ regexSource: regex.source, regexFlags: regex.flag }
|
|
17
|
+
else
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
6
20
|
end
|
|
7
21
|
|
|
8
22
|
def match?(target_url)
|
|
9
23
|
case @url
|
|
10
24
|
when String
|
|
11
|
-
|
|
25
|
+
joined_url == target_url || File.fnmatch?(@url, target_url)
|
|
12
26
|
when Regexp
|
|
13
27
|
@url.match?(target_url)
|
|
14
28
|
else
|
|
15
29
|
false
|
|
16
30
|
end
|
|
17
31
|
end
|
|
32
|
+
|
|
33
|
+
private def joined_url
|
|
34
|
+
if @base_url && !@url.start_with?('*')
|
|
35
|
+
URI.join(@base_url, @url).to_s
|
|
36
|
+
else
|
|
37
|
+
@url
|
|
38
|
+
end
|
|
39
|
+
end
|
|
18
40
|
end
|
|
19
41
|
end
|
data/lib/playwright/utils.rb
CHANGED
|
@@ -1,35 +1,70 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
|
|
1
3
|
module Playwright
|
|
2
4
|
module Utils
|
|
3
5
|
module PrepareBrowserContextOptions
|
|
4
6
|
# @see https://github.com/microsoft/playwright/blob/5a2cfdbd47ed3c3deff77bb73e5fac34241f649d/src/client/browserContext.ts#L265
|
|
5
7
|
private def prepare_browser_context_options(params)
|
|
6
|
-
if params[:
|
|
7
|
-
params.delete(:
|
|
8
|
+
if params[:noViewport] == 0
|
|
9
|
+
params.delete(:noViewport)
|
|
8
10
|
params[:noDefaultViewport] = true
|
|
9
11
|
end
|
|
10
12
|
if params[:extraHTTPHeaders]
|
|
11
|
-
|
|
13
|
+
params[:extraHTTPHeaders] = ::Playwright::HttpHeaders.new(params[:extraHTTPHeaders]).as_serialized
|
|
14
|
+
end
|
|
15
|
+
if params[:record_video_dir]
|
|
16
|
+
params[:recordVideo] = {
|
|
17
|
+
dir: params.delete(:record_video_dir)
|
|
18
|
+
}
|
|
19
|
+
if params[:record_video_size]
|
|
20
|
+
params[:recordVideo][:size] = params.delete(:record_video_size)
|
|
21
|
+
end
|
|
12
22
|
end
|
|
13
23
|
if params[:storageState].is_a?(String)
|
|
14
24
|
params[:storageState] = JSON.parse(File.read(params[:storageState]))
|
|
15
25
|
end
|
|
16
26
|
|
|
27
|
+
%i[colorScheme reducedMotion forcedColors contrast].each do |key|
|
|
28
|
+
if params[key] == 'null'
|
|
29
|
+
params[key] = 'no-override'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if params[:acceptDownloads] || params[:acceptDownloads] == false
|
|
34
|
+
params[:acceptDownloads] = params[:acceptDownloads] ? 'accept' : 'deny'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if params[:clientCertificates].is_a?(Array)
|
|
38
|
+
params[:clientCertificates] = params[:clientCertificates].filter_map do |item|
|
|
39
|
+
out_record = {
|
|
40
|
+
origin: item[:origin],
|
|
41
|
+
passphrase: item[:passphrase],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
{ pfxPath: 'pfx', certPath: 'cert', keyPath: 'key' }.each do |key, out_key|
|
|
45
|
+
if (filepath = item[key])
|
|
46
|
+
out_record[out_key] = Base64.encode64(File.read(filepath)) rescue ''
|
|
47
|
+
elsif (value = item[out_key.to_sym])
|
|
48
|
+
out_record[out_key] = value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
out_record.compact!
|
|
53
|
+
next nil if out_record.empty?
|
|
54
|
+
|
|
55
|
+
out_record
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
17
59
|
params
|
|
18
60
|
end
|
|
19
61
|
end
|
|
20
62
|
|
|
21
63
|
module Errors
|
|
22
|
-
module
|
|
64
|
+
module TargetClosedErrorMethods
|
|
23
65
|
# @param err [Exception]
|
|
24
|
-
private def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
[
|
|
28
|
-
'Browser has been closed',
|
|
29
|
-
'Target page, context or browser has been closed',
|
|
30
|
-
].any? do |closed_message|
|
|
31
|
-
err.message.end_with?(closed_message)
|
|
32
|
-
end
|
|
66
|
+
private def target_closed_error?(err)
|
|
67
|
+
err.is_a?(TargetClosedError)
|
|
33
68
|
end
|
|
34
69
|
end
|
|
35
70
|
end
|
data/lib/playwright/version.rb
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
if @page.send(:remote_connection?)
|
|
26
|
+
raise 'Path is not available when using browserType.connect(). Use save_as() to save a local copy.'
|
|
27
|
+
end
|
|
28
|
+
wait_for_artifact_and do |artifact|
|
|
29
|
+
artifact.absolute_path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save_as(path)
|
|
34
|
+
wait_for_artifact_and do |artifact|
|
|
35
|
+
artifact.save_as(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def delete
|
|
40
|
+
wait_for_artifact_and do |artifact|
|
|
41
|
+
artifact.delete
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def wait_for_artifact_and(&block)
|
|
46
|
+
artifact = @artifact.value!
|
|
47
|
+
unless artifact
|
|
48
|
+
raise 'Page did not produce any video frames'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
block.call(artifact)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module Playwright
|
|
4
|
+
# ref: https://github.com/microsoft/playwright-python/blob/v1.40.0/playwright/_impl/_waiter.py
|
|
5
|
+
# ref: https://github.com/microsoft/playwright/blob/v1.40.0/packages/playwright-core/src/client/waiter.ts
|
|
6
|
+
class Waiter
|
|
7
|
+
def initialize(channel_owner, wait_name:)
|
|
8
|
+
@result = Concurrent::Promises.resolvable_future
|
|
9
|
+
@wait_id = SecureRandom.hex(16)
|
|
10
|
+
@event = wait_name
|
|
11
|
+
@channel = channel_owner.channel
|
|
12
|
+
@registered_listeners = Set.new
|
|
13
|
+
@listeners_mutex = Mutex.new
|
|
14
|
+
@logs = []
|
|
15
|
+
wait_for_event_info_before
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def wait_for_event_info_before
|
|
19
|
+
@channel.async_send_message_to_server(
|
|
20
|
+
"waitForEventInfo",
|
|
21
|
+
{
|
|
22
|
+
"info": {
|
|
23
|
+
"waitId": @wait_id,
|
|
24
|
+
"phase": "before",
|
|
25
|
+
"event": @event,
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private def wait_for_event_info_after(error: nil)
|
|
32
|
+
@channel.async_send_message_to_server(
|
|
33
|
+
"waitForEventInfo",
|
|
34
|
+
{
|
|
35
|
+
"info": {
|
|
36
|
+
"waitId": @wait_id,
|
|
37
|
+
"phase": "after",
|
|
38
|
+
"error": error,
|
|
39
|
+
}.compact,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reject_on_event(emitter, event, error_or_proc, predicate: nil)
|
|
45
|
+
listener = -> (*args) {
|
|
46
|
+
if !predicate || predicate.call(*args)
|
|
47
|
+
if error_or_proc.respond_to?(:call)
|
|
48
|
+
reject(error_or_proc.call)
|
|
49
|
+
else
|
|
50
|
+
reject(error_or_proc)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
}
|
|
54
|
+
register_listener(emitter, event, listener)
|
|
55
|
+
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reject_on_timeout(timeout_ms, message)
|
|
60
|
+
return if timeout_ms <= 0
|
|
61
|
+
|
|
62
|
+
Concurrent::Promises.schedule(timeout_ms / 1000.0) do
|
|
63
|
+
reject(TimeoutError.new(message: message))
|
|
64
|
+
end.rescue do |err|
|
|
65
|
+
puts err, err.backtrace
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def cleanup
|
|
72
|
+
listeners = @listeners_mutex.synchronize do
|
|
73
|
+
@registered_listeners.to_a.tap { @registered_listeners.clear }
|
|
74
|
+
end
|
|
75
|
+
listeners.each do |emitter, event, listener|
|
|
76
|
+
emitter.off(event, listener)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def force_fulfill(result)
|
|
81
|
+
fulfill(result)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def force_reject(error)
|
|
85
|
+
reject(error)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private def fulfill(result)
|
|
89
|
+
cleanup
|
|
90
|
+
return if @result.resolved?
|
|
91
|
+
@result.fulfill(result)
|
|
92
|
+
wait_for_event_info_after
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def reject(error)
|
|
96
|
+
cleanup
|
|
97
|
+
return if @result.resolved?
|
|
98
|
+
klass = error.is_a?(TimeoutError) ? TimeoutError : Error
|
|
99
|
+
ex = klass.new(message: "#{error.message}#{format_log_recording(@logs)}")
|
|
100
|
+
@result.reject(ex)
|
|
101
|
+
wait_for_event_info_after(error: ex)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param [Playwright::EventEmitter]
|
|
105
|
+
# @param
|
|
106
|
+
def wait_for_event(emitter, event, predicate: nil)
|
|
107
|
+
listener = -> (*args) {
|
|
108
|
+
begin
|
|
109
|
+
if !predicate || predicate.call(*args)
|
|
110
|
+
fulfill(args.first)
|
|
111
|
+
end
|
|
112
|
+
rescue => err
|
|
113
|
+
reject(err)
|
|
114
|
+
end
|
|
115
|
+
}
|
|
116
|
+
register_listener(emitter, event, listener)
|
|
117
|
+
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private def register_listener(emitter, event, listener)
|
|
122
|
+
emitter.on(event, listener)
|
|
123
|
+
remove_later = false
|
|
124
|
+
@listeners_mutex.synchronize do
|
|
125
|
+
if @result.resolved?
|
|
126
|
+
remove_later = true
|
|
127
|
+
else
|
|
128
|
+
@registered_listeners << [emitter, event, listener]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
emitter.off(event, listener) if remove_later
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
attr_reader :result
|
|
135
|
+
|
|
136
|
+
def log(message)
|
|
137
|
+
@logs << message
|
|
138
|
+
begin
|
|
139
|
+
@channel.async_send_message_to_server(
|
|
140
|
+
"waitForEventInfo",
|
|
141
|
+
{
|
|
142
|
+
"info": {
|
|
143
|
+
"waitId": @wait_id,
|
|
144
|
+
"phase": "log",
|
|
145
|
+
"message": message,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
rescue => err
|
|
150
|
+
# ignore
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param logs [Array<String>]
|
|
155
|
+
private def format_log_recording(logs)
|
|
156
|
+
return "" if logs.empty?
|
|
157
|
+
|
|
158
|
+
header = " logs "
|
|
159
|
+
header_length = 60
|
|
160
|
+
left_length = ((header_length - header.length) / 2.0).to_i
|
|
161
|
+
right_length = header_length - header.length - left_length
|
|
162
|
+
new_line = "\n"
|
|
163
|
+
"#{new_line}#{'=' * left_length}#{header}#{'=' * right_length}#{new_line}#{logs.join(new_line)}#{new_line}#{'=' * header_length}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|