async-webdriver 0.11.0 → 0.12.1
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/lib/async/webdriver/bridge/chrome.rb +42 -11
- data/lib/async/webdriver/bridge/driver.rb +1 -1
- data/lib/async/webdriver/bridge/firefox.rb +7 -7
- data/lib/async/webdriver/bridge/generic.rb +1 -1
- data/lib/async/webdriver/bridge/pool.rb +1 -1
- data/lib/async/webdriver/bridge/process_group.rb +1 -1
- data/lib/async/webdriver/bridge/safari.rb +6 -6
- data/lib/async/webdriver/bridge.rb +1 -1
- data/lib/async/webdriver/element.rb +1 -1
- data/lib/async/webdriver/error.rb +1 -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 +1 -1
- data/lib/async/webdriver/scope/window.rb +1 -1
- data/lib/async/webdriver/scope.rb +1 -1
- data/lib/async/webdriver/session.rb +1 -1
- data/lib/async/webdriver/version.rb +2 -2
- data/license.md +1 -1
- data/readme.md +8 -0
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +6 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 243addf7d5e3f7a978552847d2ae011d34399050668c0c2fc7b6573cacc9fa7e
|
|
4
|
+
data.tar.gz: 296591c00edb0071b417a0bf70dc42ef3caed330d4155b15b81da7a2c6ae5282
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9337ecc0e8a306a5f2b4ddabaffd2cc76f03ba1cd3132cb76ef22cfca413bf63f5dd318efb1c93b864250c8238b5726551c8e9d3e20f5b78b9655f59de7912f5
|
|
7
|
+
data.tar.gz: f367473a2e748c86b3dbbfcfbe3286c09e0e9c70b8e3a44e0fa504ec9dec48f3f3446b08cdc83386dc0cc898f09a7e67122e941b52e24e5008624331ce5be8c3
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -1,10 +1,11 @@
|
|
|
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 "generic"
|
|
7
7
|
require_relative "process_group"
|
|
8
|
+
require_relative "../installer"
|
|
8
9
|
|
|
9
10
|
module Async
|
|
10
11
|
module WebDriver
|
|
@@ -21,13 +22,13 @@ module Async
|
|
|
21
22
|
# ```
|
|
22
23
|
class Chrome < Generic
|
|
23
24
|
# @returns [String] The path to the `chromedriver` executable.
|
|
24
|
-
def
|
|
25
|
-
@options.fetch(:
|
|
25
|
+
def driver_path
|
|
26
|
+
@options.fetch(:driver_path, "chromedriver")
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
# @returns [String] The version of the `chromedriver` executable.
|
|
29
30
|
def version
|
|
30
|
-
::IO.popen([self.
|
|
31
|
+
::IO.popen([self.driver_path, "--version"]) do |io|
|
|
31
32
|
return io.read
|
|
32
33
|
end
|
|
33
34
|
rescue Errno::ENOENT
|
|
@@ -46,7 +47,7 @@ module Async
|
|
|
46
47
|
# @returns [Array(String)] The arguments to pass to the `chromedriver` executable.
|
|
47
48
|
def arguments(**options)
|
|
48
49
|
[
|
|
49
|
-
options.fetch(:
|
|
50
|
+
options.fetch(:driver_path, "chromedriver"),
|
|
50
51
|
"--port=#{self.port}",
|
|
51
52
|
].compact
|
|
52
53
|
end
|
|
@@ -69,21 +70,51 @@ module Async
|
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
# Start the driver
|
|
73
|
+
# Start the driver, forwarding the bridge's own options to the driver process
|
|
74
|
+
# so that a custom `:driver_path` reaches the chromedriver executable.
|
|
73
75
|
def start(**options)
|
|
74
|
-
Driver.new(**options).tap(&:start)
|
|
76
|
+
Driver.new(**@options, **options).tap(&:start)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Ensure the given version of Chrome for Testing is installed and return a
|
|
80
|
+
# fully configured {Chrome} bridge pointing at it.
|
|
81
|
+
#
|
|
82
|
+
# Delegates to {Async::WebDriver::Installer::Chrome.install} for version
|
|
83
|
+
# resolution and download, then wraps the result in a configured bridge.
|
|
84
|
+
#
|
|
85
|
+
# @parameter version [Symbol | String] `:stable`, `:beta`, `:dev`, `:canary`,
|
|
86
|
+
# a major version string like `"148"`, or an exact version like `"148.0.7778.56"`.
|
|
87
|
+
# @parameter cache_path [String] Root of the cache directory.
|
|
88
|
+
# Default: `~/.cache/async-webdriver.rb` (XDG-compliant).
|
|
89
|
+
# @parameter options [Hash] Additional options forwarded to {.new} (e.g. `headless: false`).
|
|
90
|
+
# @returns [Chrome] A configured bridge.
|
|
91
|
+
def self.for(version = :stable, cache_path: Installer.cache_path("chrome"), **options)
|
|
92
|
+
require_relative "../installer/chrome"
|
|
93
|
+
installation = Installer::Chrome.find(version, cache_path: cache_path) || Installer::Chrome.install(version, cache_path: cache_path)
|
|
94
|
+
new(driver_path: installation.driver_path, browser_path: installation.browser_path, **options)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The path to the Chrome browser executable. If `nil`, ChromeDriver uses its own discovery.
|
|
98
|
+
# @returns [String | Nil]
|
|
99
|
+
def browser_path
|
|
100
|
+
@options[:browser_path]
|
|
75
101
|
end
|
|
76
102
|
|
|
77
103
|
# The default capabilities for the Chrome browser which need to be provided when requesting a new session.
|
|
78
104
|
# @parameter headless [Boolean] Whether to run the browser in headless mode.
|
|
105
|
+
# @parameter browser_path [String | Nil] Path to the Chrome browser executable. Overrides ChromeDriver's default discovery, useful for pointing at a specific Chrome for Testing installation.
|
|
79
106
|
# @returns [Hash] The default capabilities for the Chrome browser.
|
|
80
|
-
def default_capabilities(headless: self.headless
|
|
107
|
+
def default_capabilities(headless: self.headless?, browser_path: self.browser_path)
|
|
108
|
+
chrome_options = {
|
|
109
|
+
args: [headless ? "--headless=new" : nil].compact,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
chrome_options[:binary] = browser_path if browser_path
|
|
113
|
+
|
|
81
114
|
{
|
|
82
115
|
alwaysMatch: {
|
|
83
116
|
browserName: "chrome",
|
|
84
|
-
"goog:chromeOptions":
|
|
85
|
-
args: [headless ? "--headless=new" : nil].compact,
|
|
86
|
-
},
|
|
117
|
+
"goog:chromeOptions": chrome_options,
|
|
87
118
|
webSocketUrl: true,
|
|
88
119
|
},
|
|
89
120
|
}
|
|
@@ -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 "generic"
|
|
7
7
|
require_relative "process_group"
|
|
@@ -20,13 +20,13 @@ module Async
|
|
|
20
20
|
# end
|
|
21
21
|
class Firefox < Generic
|
|
22
22
|
# @returns [String] The path to the `geckodriver` executable.
|
|
23
|
-
def
|
|
24
|
-
@options.fetch(:
|
|
23
|
+
def driver_path
|
|
24
|
+
@options.fetch(:driver_path, "geckodriver")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @returns [String] The version of the `geckodriver` executable.
|
|
28
28
|
def version
|
|
29
|
-
::IO.popen([self.
|
|
29
|
+
::IO.popen([self.driver_path, "--version"]) do |io|
|
|
30
30
|
return io.read
|
|
31
31
|
end
|
|
32
32
|
rescue Errno::ENOENT
|
|
@@ -47,10 +47,10 @@ module Async
|
|
|
47
47
|
1
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# @returns [Array(String)] The arguments to pass to the `
|
|
50
|
+
# @returns [Array(String)] The arguments to pass to the `geckodriver` executable.
|
|
51
51
|
def arguments(**options)
|
|
52
52
|
[
|
|
53
|
-
options.fetch(:
|
|
53
|
+
options.fetch(:driver_path, "geckodriver"),
|
|
54
54
|
"--port", self.port.to_s,
|
|
55
55
|
].compact
|
|
56
56
|
end
|
|
@@ -75,7 +75,7 @@ module Async
|
|
|
75
75
|
|
|
76
76
|
# Start the driver.
|
|
77
77
|
def start(**options)
|
|
78
|
-
Driver.new(**options).tap(&:start)
|
|
78
|
+
Driver.new(**@options, **options).tap(&:start)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# The default capabilities for the Firefox browser which need to be provided when requesting a new session.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2024-
|
|
4
|
+
# Copyright, 2024-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "generic"
|
|
7
7
|
require_relative "process_group"
|
|
@@ -21,13 +21,13 @@ module Async
|
|
|
21
21
|
# ```
|
|
22
22
|
class Safari < Generic
|
|
23
23
|
# @returns [String] The path to the `safaridriver` executable.
|
|
24
|
-
def
|
|
25
|
-
@options.fetch(:
|
|
24
|
+
def driver_path
|
|
25
|
+
@options.fetch(:driver_path, "safaridriver")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# @returns [String] The version of the `safaridriver` executable.
|
|
29
29
|
def version
|
|
30
|
-
::IO.popen([self.
|
|
30
|
+
::IO.popen([self.driver_path, "--version"]) do |io|
|
|
31
31
|
return io.read
|
|
32
32
|
end
|
|
33
33
|
rescue Errno::ENOENT
|
|
@@ -46,7 +46,7 @@ module Async
|
|
|
46
46
|
# @returns [Array(String)] The arguments to pass to the `safaridriver` executable.
|
|
47
47
|
def arguments(**options)
|
|
48
48
|
[
|
|
49
|
-
options.fetch(:
|
|
49
|
+
options.fetch(:driver_path, "safaridriver"),
|
|
50
50
|
"--port=#{self.port}",
|
|
51
51
|
].compact
|
|
52
52
|
end
|
|
@@ -71,7 +71,7 @@ module Async
|
|
|
71
71
|
|
|
72
72
|
# Start the driver.
|
|
73
73
|
def start(**options)
|
|
74
|
-
Driver.new(**options).tap(&:start)
|
|
74
|
+
Driver.new(**@options, **options).tap(&:start)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# The default capabilities for the Safari browser which need to be provided when requesting a new session.
|
|
@@ -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,12 +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
6
|
# @namespace
|
|
7
7
|
module Async
|
|
8
8
|
# @namespace
|
|
9
9
|
module WebDriver
|
|
10
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.12.1"
|
|
11
11
|
end
|
|
12
12
|
end
|
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -28,6 +28,14 @@ Please see the [project documentation](https://socketry.github.io/async-webdrive
|
|
|
28
28
|
|
|
29
29
|
Please see the [project releases](https://socketry.github.io/async-webdriver/releases/index) for all releases.
|
|
30
30
|
|
|
31
|
+
### v0.12.0
|
|
32
|
+
|
|
33
|
+
- Add `Async::WebDriver::Installer::Chrome` for automatic Chrome for Testing installation and management. `Installer::Chrome.install(version)` resolves the version via the Chrome for Testing JSON API, caches binaries in `~/.local/state/async-webdriver/` (XDG `$XDG_STATE_HOME`), and returns an `Installation` with paths to both the Chrome and ChromeDriver binaries.
|
|
34
|
+
- Add `Bridge::Chrome.for(version)` as a convenience shorthand: installs the requested version if needed, then returns a fully configured `Chrome` bridge. Versions can be a channel symbol (`:stable`, `:beta`, `:dev`, `:canary`), a major version string (`"148"`), or an exact version string (`"148.0.7778.56"`).
|
|
35
|
+
- Add `bake async:webdriver:chrome:install` task for installing Chrome for Testing from the command line, e.g. in CI setup steps.
|
|
36
|
+
- Fix `Bridge::Chrome#start`, `Bridge::Firefox#start`, and `Bridge::Safari#start` not forwarding the bridge's own options (including `:driver_path`) to the driver process.
|
|
37
|
+
- Rename `path:` to `driver_path:` on `Bridge::Chrome`, `Bridge::Firefox`, and `Bridge::Safari` for consistency. Add `browser_path:` to `Bridge::Chrome` (mapped to `goog:chromeOptions.binary`) in place of the former `binary:` option, consistent with `Installer::Chrome::Installation#browser_path` and `#driver_path`.
|
|
38
|
+
|
|
31
39
|
### v0.11.0
|
|
32
40
|
|
|
33
41
|
- Add `Scope::Window` with `#window_rect`, `#resize_window`, `#set_window_rect`, `#maximize_window`, `#minimize_window`, and `#fullscreen_window`.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.12.0
|
|
4
|
+
|
|
5
|
+
- Add `Async::WebDriver::Installer::Chrome` for automatic Chrome for Testing installation and management. `Installer::Chrome.install(version)` resolves the version via the Chrome for Testing JSON API, caches binaries in `~/.local/state/async-webdriver/` (XDG `$XDG_STATE_HOME`), and returns an `Installation` with paths to both the Chrome and ChromeDriver binaries.
|
|
6
|
+
- Add `Bridge::Chrome.for(version)` as a convenience shorthand: installs the requested version if needed, then returns a fully configured `Chrome` bridge. Versions can be a channel symbol (`:stable`, `:beta`, `:dev`, `:canary`), a major version string (`"148"`), or an exact version string (`"148.0.7778.56"`).
|
|
7
|
+
- Add `bake async:webdriver:chrome:install` task for installing Chrome for Testing from the command line, e.g. in CI setup steps.
|
|
8
|
+
- Fix `Bridge::Chrome#start`, `Bridge::Firefox#start`, and `Bridge::Safari#start` not forwarding the bridge's own options (including `:driver_path`) to the driver process.
|
|
9
|
+
- Rename `path:` to `driver_path:` on `Bridge::Chrome`, `Bridge::Firefox`, and `Bridge::Safari` for consistency. Add `browser_path:` to `Bridge::Chrome` (mapped to `goog:chromeOptions.binary`) in place of the former `binary:` option, consistent with `Installer::Chrome::Installation#browser_path` and `#driver_path`.
|
|
10
|
+
|
|
3
11
|
## v0.11.0
|
|
4
12
|
|
|
5
13
|
- Add `Scope::Window` with `#window_rect`, `#resize_window`, `#set_window_rect`, `#maximize_window`, `#minimize_window`, and `#fullscreen_window`.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-webdriver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -130,6 +130,11 @@ files:
|
|
|
130
130
|
- lib/async/webdriver/client.rb
|
|
131
131
|
- lib/async/webdriver/element.rb
|
|
132
132
|
- lib/async/webdriver/error.rb
|
|
133
|
+
- lib/async/webdriver/installer.rb
|
|
134
|
+
- lib/async/webdriver/installer/chrome.rb
|
|
135
|
+
- lib/async/webdriver/installer/chrome/installation.rb
|
|
136
|
+
- lib/async/webdriver/installer/chrome/platform.rb
|
|
137
|
+
- lib/async/webdriver/installer/chrome/releases.rb
|
|
133
138
|
- lib/async/webdriver/locator.rb
|
|
134
139
|
- lib/async/webdriver/request_helper.rb
|
|
135
140
|
- lib/async/webdriver/scope.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|