async-webdriver 0.10.0 → 0.12.0
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
- checksums.yaml.gz.sig +0 -0
- data/context/navigation-timing.md +2 -2
- data/lib/async/webdriver/bridge/chrome.rb +47 -11
- data/lib/async/webdriver/bridge/driver.rb +11 -1
- data/lib/async/webdriver/bridge/firefox.rb +14 -7
- data/lib/async/webdriver/bridge/generic.rb +4 -1
- data/lib/async/webdriver/bridge/pool.rb +31 -1
- data/lib/async/webdriver/bridge/process_group.rb +6 -1
- data/lib/async/webdriver/bridge/safari.rb +12 -6
- data/lib/async/webdriver/bridge.rb +5 -1
- data/lib/async/webdriver/element.rb +3 -1
- data/lib/async/webdriver/error.rb +2 -1
- data/lib/async/webdriver/installer/chrome/installation.rb +193 -0
- data/lib/async/webdriver/installer/chrome/platform.rb +82 -0
- data/lib/async/webdriver/installer/chrome/releases.rb +113 -0
- data/lib/async/webdriver/installer/chrome.rb +66 -0
- data/lib/async/webdriver/installer.rb +37 -0
- data/lib/async/webdriver/scope/printing.rb +38 -6
- data/lib/async/webdriver/scope/window.rb +50 -0
- data/lib/async/webdriver/scope.rb +10 -1
- data/lib/async/webdriver/session.rb +3 -1
- data/lib/async/webdriver/version.rb +4 -2
- data/license.md +1 -1
- data/readme.md +29 -0
- data/releases.md +13 -0
- data.tar.gz.sig +0 -0
- metadata +9 -3
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
require_relative "platform"
|
|
9
|
+
require_relative "releases"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module WebDriver
|
|
13
|
+
module Installer
|
|
14
|
+
module Chrome
|
|
15
|
+
# Represents a Chrome for Testing installation on disk, and provides class-level
|
|
16
|
+
# methods for resolving, locating, and downloading installations.
|
|
17
|
+
#
|
|
18
|
+
# Installations are stored under the cache_path directory, organised as:
|
|
19
|
+
#
|
|
20
|
+
# {cache_path}/{platform}/{version}/
|
|
21
|
+
# chrome/ ← extracted chrome zip contents
|
|
22
|
+
# chromedriver/ ← extracted chromedriver zip contents
|
|
23
|
+
#
|
|
24
|
+
# Channel names (e.g. `stable`) are stored as symlinks pointing at the
|
|
25
|
+
# specific version directory, so that {find} can resolve them without
|
|
26
|
+
# hitting the network. {install} always re-checks the API and updates
|
|
27
|
+
# the symlink if the channel has moved on to a newer version.
|
|
28
|
+
class Installation
|
|
29
|
+
# Look up an existing installation, or download and install a fresh one.
|
|
30
|
+
#
|
|
31
|
+
# For channel specifiers (`:stable`, `:beta`, etc.), always hits the
|
|
32
|
+
# Chrome for Testing API to resolve the current version, downloads if
|
|
33
|
+
# needed, and updates the channel symlink. For exact versions, checks
|
|
34
|
+
# the local cache only.
|
|
35
|
+
#
|
|
36
|
+
# @parameter version [Symbol | String] Channel or version specifier.
|
|
37
|
+
# @parameter cache_path [String] Root of the cache directory.
|
|
38
|
+
# @returns [Installation]
|
|
39
|
+
def self.install(version, cache_path:)
|
|
40
|
+
platform = Platform.current
|
|
41
|
+
release = Releases.resolve(version, platform)
|
|
42
|
+
|
|
43
|
+
unless installation = find(release[:version], platform, cache_path: cache_path)
|
|
44
|
+
Console.info(self, "Installing Chrome for Testing #{release[:version]}...", platform: platform)
|
|
45
|
+
|
|
46
|
+
dir = installation_dir(release[:version], platform, cache_path: cache_path)
|
|
47
|
+
FileUtils.mkdir_p(dir)
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
download_and_extract(release[:chrome_url], File.join(dir, "chrome"))
|
|
51
|
+
download_and_extract(release[:chromedriver_url], File.join(dir, "chromedriver"))
|
|
52
|
+
|
|
53
|
+
installation = find(release[:version], platform, cache_path: cache_path) or
|
|
54
|
+
raise "Installation failed: binaries not found after extraction"
|
|
55
|
+
|
|
56
|
+
Console.info(self, "Installed Chrome for Testing #{release[:version]}.", platform: platform)
|
|
57
|
+
rescue
|
|
58
|
+
FileUtils.rm_rf(dir)
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Update the channel symlink so subsequent find(:stable) calls
|
|
64
|
+
# resolve locally without a network request.
|
|
65
|
+
if channel = channel_name(version)
|
|
66
|
+
update_channel_symlink(channel, release[:version], platform, cache_path: cache_path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return installation
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Find an already-installed version or channel, without hitting the network.
|
|
73
|
+
#
|
|
74
|
+
# For channel names (`:stable`, `"stable"`, etc.), resolves the local
|
|
75
|
+
# symlink. For exact versions, checks the installation directory directly.
|
|
76
|
+
#
|
|
77
|
+
# @parameter version [Symbol | String] Channel or exact version string.
|
|
78
|
+
# @parameter platform [String] Platform string, e.g. `"mac-arm64"`.
|
|
79
|
+
# @parameter cache_path [String] Root of the cache directory.
|
|
80
|
+
# @returns [Installation | Nil]
|
|
81
|
+
def self.find(version, platform, cache_path:)
|
|
82
|
+
if channel = channel_name(version)
|
|
83
|
+
find_channel(channel, platform, cache_path: cache_path)
|
|
84
|
+
else
|
|
85
|
+
find_version(version, platform, cache_path: cache_path)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @parameter browser_path [String] Absolute path to the Chrome browser executable.
|
|
90
|
+
# @parameter driver_path [String] Absolute path to the chromedriver executable.
|
|
91
|
+
# @parameter version [String] Exact version string.
|
|
92
|
+
# @parameter platform [String] Platform string.
|
|
93
|
+
def initialize(browser_path:, driver_path:, version:, platform:)
|
|
94
|
+
@browser_path = browser_path
|
|
95
|
+
@driver_path = driver_path
|
|
96
|
+
@version = version
|
|
97
|
+
@platform = platform
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @attribute [String] Absolute path to the Chrome browser executable.
|
|
101
|
+
attr :browser_path
|
|
102
|
+
|
|
103
|
+
# @attribute [String] Absolute path to the chromedriver executable.
|
|
104
|
+
attr :driver_path
|
|
105
|
+
|
|
106
|
+
# @attribute [String] Exact installed version, e.g. `"148.0.7778.56"`.
|
|
107
|
+
attr :version
|
|
108
|
+
|
|
109
|
+
# @attribute [String] Platform, e.g. `"mac-arm64"`.
|
|
110
|
+
attr :platform
|
|
111
|
+
|
|
112
|
+
private_class_method def self.channel_name(version)
|
|
113
|
+
Releases::CHANNELS.key(version.to_s.capitalize) && version.to_s.downcase
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private_class_method def self.find_channel(channel, platform, cache_path:)
|
|
117
|
+
symlink = channel_symlink(channel, platform, cache_path: cache_path)
|
|
118
|
+
return nil unless File.symlink?(symlink)
|
|
119
|
+
|
|
120
|
+
# Derive the version from the symlink target name.
|
|
121
|
+
version = File.basename(File.readlink(symlink))
|
|
122
|
+
find_version(version, platform, cache_path: cache_path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private_class_method def self.find_version(version, platform, cache_path:)
|
|
126
|
+
dir = installation_dir(version, platform, cache_path: cache_path)
|
|
127
|
+
|
|
128
|
+
browser_path = File.join(dir, "chrome", Platform.chrome_binary(platform))
|
|
129
|
+
driver_path = File.join(dir, "chromedriver", Platform.chromedriver_binary(platform))
|
|
130
|
+
|
|
131
|
+
return nil unless File.exist?(browser_path) && File.exist?(driver_path)
|
|
132
|
+
|
|
133
|
+
new(
|
|
134
|
+
browser_path: browser_path,
|
|
135
|
+
driver_path: driver_path,
|
|
136
|
+
version: version,
|
|
137
|
+
platform: platform,
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private_class_method def self.update_channel_symlink(channel, version, platform, cache_path:)
|
|
142
|
+
symlink = channel_symlink(channel, platform, cache_path: cache_path)
|
|
143
|
+
target = installation_dir(version, platform, cache_path: cache_path)
|
|
144
|
+
|
|
145
|
+
# Remove stale symlink if it points elsewhere.
|
|
146
|
+
if File.symlink?(symlink) && File.readlink(symlink) != target
|
|
147
|
+
File.unlink(symlink)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
File.symlink(target, symlink) unless File.symlink?(symlink)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private_class_method def self.channel_symlink(channel, platform, cache_path:)
|
|
154
|
+
File.join(cache_path, platform, channel.to_s)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private_class_method def self.installation_dir(version, platform, cache_path:)
|
|
158
|
+
File.join(cache_path, platform, version)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private_class_method def self.download_and_extract(url, dest)
|
|
162
|
+
require "async/http/internet"
|
|
163
|
+
|
|
164
|
+
Tempfile.create(["async-webdriver-", ".zip"]) do |tmp|
|
|
165
|
+
tmp.binmode
|
|
166
|
+
|
|
167
|
+
Sync do
|
|
168
|
+
internet = Async::HTTP::Internet.new
|
|
169
|
+
begin
|
|
170
|
+
Console.debug(self, "Downloading...", url: url)
|
|
171
|
+
response = internet.get(url)
|
|
172
|
+
tmp.write(response.read)
|
|
173
|
+
tmp.flush
|
|
174
|
+
ensure
|
|
175
|
+
internet.close
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
FileUtils.mkdir_p(dest)
|
|
180
|
+
system("unzip", "-q", "-o", tmp.path, "-d", dest) or
|
|
181
|
+
raise "Failed to extract #{url}"
|
|
182
|
+
|
|
183
|
+
# Remove macOS quarantine attributes added to files downloaded via code.
|
|
184
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
185
|
+
system("xattr", "-r", "-d", "com.apple.quarantine", dest)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module WebDriver
|
|
8
|
+
module Installer
|
|
9
|
+
module Chrome
|
|
10
|
+
# Platform detection for Chrome for Testing downloads.
|
|
11
|
+
#
|
|
12
|
+
# Maps Ruby's `RUBY_PLATFORM` to the platform strings used by the
|
|
13
|
+
# Chrome for Testing JSON API and zip file naming conventions.
|
|
14
|
+
module Platform
|
|
15
|
+
# Ordered list of (pattern, platform) pairs. First match wins.
|
|
16
|
+
PLATFORM_MAP = [
|
|
17
|
+
[/arm.*darwin|darwin.*arm|aarch64.*darwin|darwin.*aarch64/, "mac-arm64"],
|
|
18
|
+
[/darwin/, "mac-x64"],
|
|
19
|
+
[/aarch64.*linux|linux.*aarch64/, "linux-arm64"],
|
|
20
|
+
[/linux/, "linux64"],
|
|
21
|
+
[/x64.*mingw|mingw.*x64/, "win64"],
|
|
22
|
+
[/mingw/, "win32"],
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Detect the current platform.
|
|
26
|
+
# @returns [String] e.g. `"mac-arm64"`, `"linux64"`.
|
|
27
|
+
# @raises [RuntimeError] If the platform is not recognised.
|
|
28
|
+
def self.current
|
|
29
|
+
PLATFORM_MAP.each do |pattern, platform|
|
|
30
|
+
return platform if RUBY_PLATFORM.match?(pattern)
|
|
31
|
+
end
|
|
32
|
+
raise "Unsupported platform: #{RUBY_PLATFORM}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Relative path to the Chrome binary inside the extracted chrome zip.
|
|
36
|
+
# @parameter platform [String]
|
|
37
|
+
# @returns [String]
|
|
38
|
+
def self.chrome_binary(platform)
|
|
39
|
+
case platform
|
|
40
|
+
when "mac-arm64"
|
|
41
|
+
"chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
|
|
42
|
+
when "mac-x64"
|
|
43
|
+
"chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
|
|
44
|
+
when "linux64"
|
|
45
|
+
"chrome-linux64/chrome"
|
|
46
|
+
when "linux-arm64"
|
|
47
|
+
"chrome-linux-arm64/chrome"
|
|
48
|
+
when "win64"
|
|
49
|
+
"chrome-win64/chrome.exe"
|
|
50
|
+
when "win32"
|
|
51
|
+
"chrome-win32/chrome.exe"
|
|
52
|
+
else
|
|
53
|
+
raise "Unknown platform: #{platform}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Relative path to the chromedriver binary inside the extracted chromedriver zip.
|
|
58
|
+
# @parameter platform [String]
|
|
59
|
+
# @returns [String]
|
|
60
|
+
def self.chromedriver_binary(platform)
|
|
61
|
+
case platform
|
|
62
|
+
when "mac-arm64"
|
|
63
|
+
"chromedriver-mac-arm64/chromedriver"
|
|
64
|
+
when "mac-x64"
|
|
65
|
+
"chromedriver-mac-x64/chromedriver"
|
|
66
|
+
when "linux64"
|
|
67
|
+
"chromedriver-linux64/chromedriver"
|
|
68
|
+
when "linux-arm64"
|
|
69
|
+
"chromedriver-linux-arm64/chromedriver"
|
|
70
|
+
when "win64"
|
|
71
|
+
"chromedriver-win64/chromedriver.exe"
|
|
72
|
+
when "win32"
|
|
73
|
+
"chromedriver-win32/chromedriver.exe"
|
|
74
|
+
else
|
|
75
|
+
raise "Unknown platform: #{platform}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module WebDriver
|
|
10
|
+
module Installer
|
|
11
|
+
module Chrome
|
|
12
|
+
# Resolves Chrome for Testing version specifiers and download URLs using the
|
|
13
|
+
# public Chrome for Testing JSON API.
|
|
14
|
+
module Releases
|
|
15
|
+
# Returns the latest known-good version for each release channel.
|
|
16
|
+
CHANNELS_URL = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
|
|
17
|
+
|
|
18
|
+
# Returns every known-good version with its download URLs.
|
|
19
|
+
VERSIONS_URL = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"
|
|
20
|
+
|
|
21
|
+
# Maps symbolic channel names to the API's title-case keys.
|
|
22
|
+
CHANNELS = {
|
|
23
|
+
stable: "Stable",
|
|
24
|
+
beta: "Beta",
|
|
25
|
+
dev: "Dev",
|
|
26
|
+
canary: "Canary",
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Resolve a version specifier and platform to a version string and download URLs.
|
|
30
|
+
#
|
|
31
|
+
# @parameter version [Symbol | String] `:stable`, `:beta`, `:dev`, `:canary`,
|
|
32
|
+
# a major version string like `"148"`, or an exact version like `"148.0.7778.56"`.
|
|
33
|
+
# @parameter platform [String] A Chrome for Testing platform string, e.g. `"mac-arm64"`.
|
|
34
|
+
# @returns [Hash] `{ version:, chrome_url:, chromedriver_url: }`
|
|
35
|
+
def self.resolve(version, platform)
|
|
36
|
+
case version
|
|
37
|
+
when Symbol then resolve_channel(version, platform)
|
|
38
|
+
when /\A(stable|beta|dev|canary)\z/ then resolve_channel(version.to_sym, platform)
|
|
39
|
+
when /\A\d+\z/ then resolve_major(version, platform)
|
|
40
|
+
else resolve_exact(version, platform)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def self.fetch_json(url)
|
|
47
|
+
require "async/http/internet"
|
|
48
|
+
|
|
49
|
+
Sync do
|
|
50
|
+
internet = Async::HTTP::Internet.new
|
|
51
|
+
begin
|
|
52
|
+
response = internet.get(url)
|
|
53
|
+
JSON.parse(response.read)
|
|
54
|
+
ensure
|
|
55
|
+
internet.close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.resolve_channel(channel, platform)
|
|
61
|
+
key = CHANNELS.fetch(channel) do
|
|
62
|
+
raise ArgumentError, "Unknown channel #{channel.inspect}. Expected one of: #{CHANNELS.keys.inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
data = fetch_json(CHANNELS_URL)
|
|
66
|
+
entry = data.dig("channels", key) or raise "Channel #{key} not found in API response"
|
|
67
|
+
|
|
68
|
+
extract(entry, platform)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.resolve_major(major, platform)
|
|
72
|
+
data = fetch_json(VERSIONS_URL)
|
|
73
|
+
|
|
74
|
+
entry = data["versions"]
|
|
75
|
+
.select{|v| v["version"].start_with?("#{major}.")}
|
|
76
|
+
.max_by{|v| Gem::Version.new(v["version"])}
|
|
77
|
+
|
|
78
|
+
raise "No version found for major version #{major}" unless entry
|
|
79
|
+
|
|
80
|
+
extract(entry, platform)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.resolve_exact(version, platform)
|
|
84
|
+
data = fetch_json(VERSIONS_URL)
|
|
85
|
+
|
|
86
|
+
entry = data["versions"].find{|v| v["version"] == version}
|
|
87
|
+
raise "Version #{version} not found" unless entry
|
|
88
|
+
|
|
89
|
+
extract(entry, platform)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.extract(entry, platform)
|
|
93
|
+
version = entry["version"]
|
|
94
|
+
downloads = entry["downloads"]
|
|
95
|
+
|
|
96
|
+
chrome_url = downloads["chrome"]
|
|
97
|
+
&.find{|d| d["platform"] == platform}
|
|
98
|
+
&.dig("url")
|
|
99
|
+
|
|
100
|
+
chromedriver_url = downloads["chromedriver"]
|
|
101
|
+
&.find{|d| d["platform"] == platform}
|
|
102
|
+
&.dig("url")
|
|
103
|
+
|
|
104
|
+
raise "No Chrome download for platform #{platform} in version #{version}" unless chrome_url
|
|
105
|
+
raise "No ChromeDriver download for platform #{platform} in version #{version}" unless chromedriver_url
|
|
106
|
+
|
|
107
|
+
{version: version, chrome_url: chrome_url, chromedriver_url: chromedriver_url}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "chrome/platform"
|
|
7
|
+
require_relative "chrome/releases"
|
|
8
|
+
require_relative "chrome/installation"
|
|
9
|
+
|
|
10
|
+
module Async
|
|
11
|
+
module WebDriver
|
|
12
|
+
module Installer
|
|
13
|
+
# Installer for Chrome for Testing, the purpose-built Chrome variant
|
|
14
|
+
# designed for automated testing.
|
|
15
|
+
#
|
|
16
|
+
# Versions can be specified as:
|
|
17
|
+
# - A channel symbol: `:stable`, `:beta`, `:dev`, `:canary`
|
|
18
|
+
# - A major version string: `"148"` (resolves to the latest patch)
|
|
19
|
+
# - An exact version string: `"148.0.7778.56"`
|
|
20
|
+
#
|
|
21
|
+
# Installations are cached in `~/.cache/async-webdriver.rb/` by default
|
|
22
|
+
# (respects `$XDG_CACHE_HOME`).
|
|
23
|
+
#
|
|
24
|
+
# ## Example
|
|
25
|
+
#
|
|
26
|
+
# ``` ruby
|
|
27
|
+
# installation = Async::WebDriver::Installer::Chrome.install(:stable)
|
|
28
|
+
# bridge = Async::WebDriver::Bridge::Chrome.new(
|
|
29
|
+
# driver_path: installation.driver_path,
|
|
30
|
+
# browser_path: installation.browser_path,
|
|
31
|
+
# )
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# Or via the convenience shorthand on the bridge:
|
|
35
|
+
#
|
|
36
|
+
# ``` ruby
|
|
37
|
+
# bridge = Async::WebDriver::Bridge::Chrome.for(:stable)
|
|
38
|
+
# ```
|
|
39
|
+
module Chrome
|
|
40
|
+
# Default cache directory, following the XDG Base Directory Specification.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Ensure the given version is installed and return an {Installation}.
|
|
44
|
+
#
|
|
45
|
+
# Checks the local cache first; downloads from the Chrome for Testing
|
|
46
|
+
# infrastructure only when the version is not already present.
|
|
47
|
+
#
|
|
48
|
+
# @parameter version [Symbol | String] Version specifier.
|
|
49
|
+
# @parameter cache_path [String] Root of the cache directory.
|
|
50
|
+
# @returns [Installation]
|
|
51
|
+
def self.install(version = :stable, cache_path: Installer.cache_path("chrome"))
|
|
52
|
+
Installation.install(version, cache_path: cache_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Find an already-installed version or channel without hitting the network.
|
|
56
|
+
#
|
|
57
|
+
# @parameter version [Symbol | String] Channel or exact version string.
|
|
58
|
+
# @parameter cache_path [String] Root of the cache directory.
|
|
59
|
+
# @returns [Installation | Nil]
|
|
60
|
+
def self.find(version, cache_path: Installer.cache_path("chrome"))
|
|
61
|
+
Installation.find(version, Platform.current, cache_path: cache_path)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "installer/chrome"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module WebDriver
|
|
10
|
+
# Browser installation and management for automated testing.
|
|
11
|
+
#
|
|
12
|
+
# Each browser has its own sub-module with browser-specific platform detection,
|
|
13
|
+
# version resolution, and download logic:
|
|
14
|
+
#
|
|
15
|
+
# - {Installer::Chrome} — Chrome for Testing, via the Chrome for Testing JSON API.
|
|
16
|
+
module Installer
|
|
17
|
+
# Resolve the cache path for the given sub-directory.
|
|
18
|
+
#
|
|
19
|
+
# Follows the XDG Base Directory Specification, using `$XDG_CACHE_HOME`
|
|
20
|
+
# (default: `~/.cache`) as the root, with `async-webdriver.rb` as the
|
|
21
|
+
# application directory.
|
|
22
|
+
#
|
|
23
|
+
# @parameter subdirectory [String | Nil] Optional sub-directory, e.g. `"chrome"`.
|
|
24
|
+
# @parameter env [Hash] Environment to read `XDG_CACHE_HOME` from. Default: `ENV`.
|
|
25
|
+
# @returns [String] Absolute path.
|
|
26
|
+
def self.cache_path(subdirectory = nil, env = ENV)
|
|
27
|
+
path = File.expand_path("async-webdriver.rb", env.fetch("XDG_CACHE_HOME", "~/.cache"))
|
|
28
|
+
|
|
29
|
+
if subdirectory
|
|
30
|
+
path = File.join(path, subdirectory)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return path
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "base64"
|
|
7
7
|
|
|
8
8
|
module Async
|
|
9
9
|
module WebDriver
|
|
10
10
|
module Scope
|
|
11
|
-
# Helpers for
|
|
11
|
+
# Helpers for printing the current page to PDF.
|
|
12
12
|
module Printing
|
|
13
|
-
# Print the current page
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# Print the current page as a PDF and return the raw binary data.
|
|
14
|
+
#
|
|
15
|
+
# All margin and page measurements are in centimetres. The W3C WebDriver
|
|
16
|
+
# default page size is US Letter (21.59 × 27.94 cm) with 1 cm margins.
|
|
17
|
+
#
|
|
18
|
+
# @parameter orientation [String | Nil] `"portrait"` or `"landscape"`. Default: `"portrait"`.
|
|
19
|
+
# @parameter scale [Float | Nil] Scaling factor between 0.1 and 2.0. Default: `1.0`.
|
|
20
|
+
# @parameter background [Boolean | Nil] Whether to print background graphics and colours. Default: `false`.
|
|
21
|
+
# @parameter page [Hash | Nil] Page dimensions in cm. Keys: `:width`, `:height`.
|
|
22
|
+
# @parameter margin [Hash | Nil] Page margins in cm. Keys: `:top`, `:bottom`, `:left`, `:right`.
|
|
23
|
+
# @parameter page_ranges [Array(String) | Nil] Page ranges to print, e.g. `["1-5", "8"]`. Default: all pages.
|
|
24
|
+
# @parameter shrink_to_fit [Boolean | Nil] Whether to shrink content to fit the page. Default: `true`.
|
|
25
|
+
# @returns [String] The raw PDF binary data.
|
|
26
|
+
def print(orientation: nil, scale: nil, background: nil, page: nil, margin: nil, page_ranges: nil, shrink_to_fit: nil)
|
|
27
|
+
parameters = {
|
|
28
|
+
orientation: orientation,
|
|
29
|
+
scale: scale,
|
|
30
|
+
background: background,
|
|
31
|
+
page: page,
|
|
32
|
+
margin: margin,
|
|
33
|
+
pageRanges: page_ranges,
|
|
34
|
+
shrinkToFit: shrink_to_fit,
|
|
35
|
+
}.compact
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
# Synchronise with Chrome's rendering pipeline before issuing the print
|
|
38
|
+
# command. The underlying CDP call (Page.printToPDF) is synchronous: if
|
|
39
|
+
# the renderer process has not yet fully initialised its print pipeline
|
|
40
|
+
# by the time the command arrives, Chrome returns JSON-RPC error -32000
|
|
41
|
+
# ("Printing failed") with no retry. A JavaScript round-trip forces
|
|
42
|
+
# ChromeDriver to wait for the renderer to be live (a JS execution
|
|
43
|
+
# context must exist), which also guarantees the print pipeline is ready.
|
|
44
|
+
# Without this, fast-loading pages can trigger the race intermittently.
|
|
45
|
+
session.execute("return document.readyState")
|
|
46
|
+
|
|
47
|
+
reply = session.post("print", parameters)
|
|
48
|
+
|
|
49
|
+
return Base64.decode64(reply)
|
|
18
50
|
end
|
|
19
51
|
end
|
|
20
52
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module WebDriver
|
|
8
|
+
module Scope
|
|
9
|
+
# Helpers for managing the browser window size and position.
|
|
10
|
+
module Window
|
|
11
|
+
# Get the current window rect (position and size).
|
|
12
|
+
# @returns [Hash] The window rect with keys `"x"`, `"y"`, `"width"`, `"height"`.
|
|
13
|
+
def window_rect
|
|
14
|
+
session.get("window/rect")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set the window rect (position and/or size).
|
|
18
|
+
# @parameter x [Integer | Nil] The x position of the window.
|
|
19
|
+
# @parameter y [Integer | Nil] The y position of the window.
|
|
20
|
+
# @parameter width [Integer | Nil] The width of the window in CSS pixels.
|
|
21
|
+
# @parameter height [Integer | Nil] The height of the window in CSS pixels.
|
|
22
|
+
def set_window_rect(x: nil, y: nil, width: nil, height: nil)
|
|
23
|
+
session.post("window/rect", {x: x, y: y, width: width, height: height}.compact)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Resize the browser window to the given dimensions.
|
|
27
|
+
# @parameter width [Integer] The new width in CSS pixels.
|
|
28
|
+
# @parameter height [Integer] The new height in CSS pixels.
|
|
29
|
+
def resize_window(width, height)
|
|
30
|
+
set_window_rect(width: width, height: height)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Maximize the browser window.
|
|
34
|
+
def maximize_window
|
|
35
|
+
session.post("window/maximize")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Minimize the browser window.
|
|
39
|
+
def minimize_window
|
|
40
|
+
session.post("window/minimize")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Make the browser window fullscreen.
|
|
44
|
+
def fullscreen_window
|
|
45
|
+
session.post("window/fullscreen")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "scope/alerts"
|
|
7
7
|
require_relative "scope/cookies"
|
|
@@ -13,3 +13,12 @@ require_relative "scope/navigation"
|
|
|
13
13
|
require_relative "scope/printing"
|
|
14
14
|
require_relative "scope/screen_capture"
|
|
15
15
|
require_relative "scope/timeouts"
|
|
16
|
+
require_relative "scope/window"
|
|
17
|
+
|
|
18
|
+
module Async
|
|
19
|
+
module WebDriver
|
|
20
|
+
# @namespace
|
|
21
|
+
module Scope
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "request_helper"
|
|
7
7
|
require_relative "element"
|
|
@@ -56,6 +56,7 @@ module Async
|
|
|
56
56
|
@options = options
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# @returns [String] A concise representation of the session.
|
|
59
60
|
def inspect
|
|
60
61
|
"\#<#{self.class} id=#{@id.inspect}>"
|
|
61
62
|
end
|
|
@@ -126,6 +127,7 @@ module Async
|
|
|
126
127
|
include Scope::Printing
|
|
127
128
|
include Scope::ScreenCapture
|
|
128
129
|
include Scope::Timeouts
|
|
130
|
+
include Scope::Window
|
|
129
131
|
|
|
130
132
|
# Reset the session to a clean state.
|
|
131
133
|
def reset!
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
# @namespace
|
|
6
7
|
module Async
|
|
8
|
+
# @namespace
|
|
7
9
|
module WebDriver
|
|
8
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.12.0"
|
|
9
11
|
end
|
|
10
12
|
end
|
data/license.md
CHANGED