Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ecef914
Add Installer::Chrome and standardise driver_path:/browser_path: acro…
samuel-williams-shopify Apr 29, 2026
2a5171d
Add bake async:webdriver:chrome:install task.
samuel-williams-shopify Apr 29, 2026
fa4b307
Update release notes with bake task.
samuel-williams-shopify Apr 29, 2026
a3cda94
Handle channel strings in Releases.resolve; remove conversion from ba…
samuel-williams-shopify Apr 29, 2026
48855dd
Return installation from bake task.
samuel-williams-shopify Apr 29, 2026
d3fb33c
Move requires to top of installation.rb.
samuel-williams-shopify Apr 29, 2026
6ba174b
Rename info to release; remove aligned = throughout installation.rb.
samuel-williams-shopify Apr 29, 2026
dfe8e0f
Restructure install to use unless/assign pattern with single return.
samuel-williams-shopify Apr 29, 2026
bd79e40
Remove aligned keyword arguments in bake task.
samuel-williams-shopify Apr 29, 2026
28f3082
Remove aligned columns in PLATFORM_MAP.
samuel-williams-shopify Apr 29, 2026
3483db8
Expand case/when branches to multi-line in platform.rb.
samuel-williams-shopify Apr 29, 2026
bd881eb
RuboCop.
samuel-williams-shopify Apr 29, 2026
cd6dc81
Use channel symlinks to avoid network requests on repeated Chrome.for…
samuel-williams-shopify Apr 29, 2026
0fa1e3c
Add tests for Installer::Chrome::{Platform, Releases, Installation}.
samuel-williams-shopify Apr 29, 2026
1748477
Simplify DEFAULT_STATE using File.expand_path dir argument.
samuel-williams-shopify Apr 29, 2026
1b4bd34
Use XDG_CACHE_HOME and async-webdriver.rb directory name.
samuel-williams-shopify Apr 29, 2026
0719936
Replace DEFAULT_CACHE with Installer.cache_path(subdirectory, env = E…
samuel-williams-shopify Apr 29, 2026
a8a876d
Update release notes.
samuel-williams-shopify Apr 29, 2026
807b03c
RuboCop.
samuel-williams-shopify Apr 29, 2026
0432582
Rename state:/cache: to cache_path: throughout; use after block in te…
samuel-williams-shopify Apr 29, 2026
31d1ea4
Minor test tweaks.
samuel-williams-shopify Apr 29, 2026
2452bc3
Copyrights.
samuel-williams-shopify Apr 29, 2026
9552b44
Remove Bridge::Chrome.install; use Installer::Chrome.install directly.
samuel-williams-shopify Apr 29, 2026
e765758
RuboCop.
samuel-williams-shopify Apr 29, 2026
942460e
Fix test assertions: use be =~ and be(:start_with?) for Sus compatibi…
samuel-williams-shopify Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions bake/async/webdriver/chrome.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

# Install Chrome for Testing and its matching ChromeDriver.
#
# Downloads the requested version from the Chrome for Testing infrastructure
# and caches it in `~/.cache/async-webdriver.rb/` (XDG `$XDG_CACHE_HOME`).
# Subsequent calls with the same version are a no-op.
#
# @parameter version [String] The version to install: a channel (`stable`, `beta`, `dev`, `canary`),
# a major version (e.g. `148`), or an exact version (e.g. `148.0.7778.56`). Default: `stable`.
def install(version: "stable")
require "async/webdriver/installer/chrome"

installation = Async::WebDriver::Installer::Chrome.install(version)

Console.info(self, "Chrome for Testing is ready.",
version: installation.version,
platform: installation.platform,
browser_path: installation.browser_path,
driver_path: installation.driver_path,
)

return installation
end
50 changes: 40 additions & 10 deletions lib/async/webdriver/bridge/chrome.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ module Bridge
# ```
class Chrome < Generic
# @returns [String] The path to the `chromedriver` executable.
def path
@options.fetch(:path, "chromedriver")
def driver_path
@options.fetch(:driver_path, "chromedriver")
end

# @returns [String] The version of the `chromedriver` executable.
def version
::IO.popen([self.path, "--version"]) do |io|
::IO.popen([self.driver_path, "--version"]) do |io|
return io.read
end
rescue Errno::ENOENT
Expand All @@ -46,7 +46,7 @@ def initialize(**options)
# @returns [Array(String)] The arguments to pass to the `chromedriver` executable.
def arguments(**options)
[
options.fetch(:path, "chromedriver"),
options.fetch(:driver_path, "chromedriver"),
"--port=#{self.port}",
].compact
end
Expand All @@ -69,21 +69,51 @@ def close
end
end

# Start the driver.
# Start the driver, forwarding the bridge's own options to the driver process
# so that a custom `:driver_path` reaches the chromedriver executable.
def start(**options)
Driver.new(**options).tap(&:start)
Driver.new(**@options, **options).tap(&:start)
end

# Ensure the given version of Chrome for Testing is installed and return a
# fully configured {Chrome} bridge pointing at it.
#
# Delegates to {Async::WebDriver::Installer::Chrome.install} for version
# resolution and download, then wraps the result in a configured bridge.
#
# @parameter version [Symbol | String] `:stable`, `:beta`, `:dev`, `:canary`,
# a major version string like `"148"`, or an exact version like `"148.0.7778.56"`.
# @parameter cache_path [String] Root of the cache directory.
# Default: `~/.cache/async-webdriver.rb` (XDG-compliant).
# @parameter options [Hash] Additional options forwarded to {.new} (e.g. `headless: false`).
# @returns [Chrome] A configured bridge.
def self.for(version = :stable, cache_path: Installer.cache_path("chrome"), **options)
require_relative "../installer/chrome"
installation = Installer::Chrome.find(version, cache_path: cache_path) || Installer::Chrome.install(version, cache_path: cache_path)
new(driver_path: installation.driver_path, browser_path: installation.browser_path, **options)
end

# The path to the Chrome browser executable. If `nil`, ChromeDriver uses its own discovery.
# @returns [String | Nil]
def browser_path
@options[:browser_path]
end

# The default capabilities for the Chrome browser which need to be provided when requesting a new session.
# @parameter headless [Boolean] Whether to run the browser in headless mode.
# @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.
# @returns [Hash] The default capabilities for the Chrome browser.
def default_capabilities(headless: self.headless?)
def default_capabilities(headless: self.headless?, browser_path: self.browser_path)
chrome_options = {
args: [headless ? "--headless=new" : nil].compact,
}

chrome_options[:binary] = browser_path if browser_path

{
alwaysMatch: {
browserName: "chrome",
"goog:chromeOptions": {
args: [headless ? "--headless=new" : nil].compact,
},
"goog:chromeOptions": chrome_options,
webSocketUrl: true,
},
}
Expand Down
12 changes: 6 additions & 6 deletions lib/async/webdriver/bridge/firefox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ module Bridge
# end
class Firefox < Generic
# @returns [String] The path to the `geckodriver` executable.
def path
@options.fetch(:path, "geckodriver")
def driver_path
@options.fetch(:driver_path, "geckodriver")
end

# @returns [String] The version of the `geckodriver` executable.
def version
::IO.popen([self.path, "--version"]) do |io|
::IO.popen([self.driver_path, "--version"]) do |io|
return io.read
end
rescue Errno::ENOENT
Expand All @@ -47,10 +47,10 @@ def concurrency
1
end

# @returns [Array(String)] The arguments to pass to the `chromedriver` executable.
# @returns [Array(String)] The arguments to pass to the `geckodriver` executable.
def arguments(**options)
[
options.fetch(:path, "geckodriver"),
options.fetch(:driver_path, "geckodriver"),
"--port", self.port.to_s,
].compact
end
Expand All @@ -75,7 +75,7 @@ def close

# Start the driver.
def start(**options)
Driver.new(**options).tap(&:start)
Driver.new(**@options, **options).tap(&:start)
end

# The default capabilities for the Firefox browser which need to be provided when requesting a new session.
Expand Down
10 changes: 5 additions & 5 deletions lib/async/webdriver/bridge/safari.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ module Bridge
# ```
class Safari < Generic
# @returns [String] The path to the `safaridriver` executable.
def path
@options.fetch(:path, "safaridriver")
def driver_path
@options.fetch(:driver_path, "safaridriver")
end

# @returns [String] The version of the `safaridriver` executable.
def version
::IO.popen([self.path, "--version"]) do |io|
::IO.popen([self.driver_path, "--version"]) do |io|
return io.read
end
rescue Errno::ENOENT
Expand All @@ -46,7 +46,7 @@ def initialize(**options)
# @returns [Array(String)] The arguments to pass to the `safaridriver` executable.
def arguments(**options)
[
options.fetch(:path, "safaridriver"),
options.fetch(:driver_path, "safaridriver"),
"--port=#{self.port}",
].compact
end
Expand All @@ -71,7 +71,7 @@ def close

# Start the driver.
def start(**options)
Driver.new(**options).tap(&:start)
Driver.new(**@options, **options).tap(&:start)
end

# The default capabilities for the Safari browser which need to be provided when requesting a new session.
Expand Down
37 changes: 37 additions & 0 deletions lib/async/webdriver/installer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require_relative "installer/chrome"

module Async
module WebDriver
# Browser installation and management for automated testing.
#
# Each browser has its own sub-module with browser-specific platform detection,
# version resolution, and download logic:
#
# - {Installer::Chrome} — Chrome for Testing, via the Chrome for Testing JSON API.
module Installer
# Resolve the cache path for the given sub-directory.
#
# Follows the XDG Base Directory Specification, using `$XDG_CACHE_HOME`
# (default: `~/.cache`) as the root, with `async-webdriver.rb` as the
# application directory.
#
# @parameter subdirectory [String | Nil] Optional sub-directory, e.g. `"chrome"`.
# @parameter env [Hash] Environment to read `XDG_CACHE_HOME` from. Default: `ENV`.
# @returns [String] Absolute path.
def self.cache_path(subdirectory = nil, env = ENV)
path = File.expand_path("async-webdriver.rb", env.fetch("XDG_CACHE_HOME", "~/.cache"))

if subdirectory
path = File.join(path, subdirectory)
end

return path
end
end
end
end
66 changes: 66 additions & 0 deletions lib/async/webdriver/installer/chrome.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require_relative "chrome/platform"
require_relative "chrome/releases"
require_relative "chrome/installation"

module Async
module WebDriver
module Installer
# Installer for Chrome for Testing, the purpose-built Chrome variant
# designed for automated testing.
#
# Versions can be specified as:
# - A channel symbol: `:stable`, `:beta`, `:dev`, `:canary`
# - A major version string: `"148"` (resolves to the latest patch)
# - An exact version string: `"148.0.7778.56"`
#
# Installations are cached in `~/.cache/async-webdriver.rb/` by default
# (respects `$XDG_CACHE_HOME`).
#
# ## Example
#
# ``` ruby
# installation = Async::WebDriver::Installer::Chrome.install(:stable)
# bridge = Async::WebDriver::Bridge::Chrome.new(
# driver_path: installation.driver_path,
# browser_path: installation.browser_path,
# )
# ```
#
# Or via the convenience shorthand on the bridge:
#
# ``` ruby
# bridge = Async::WebDriver::Bridge::Chrome.for(:stable)
# ```
module Chrome
# Default cache directory, following the XDG Base Directory Specification.


# Ensure the given version is installed and return an {Installation}.
#
# Checks the local cache first; downloads from the Chrome for Testing
# infrastructure only when the version is not already present.
#
# @parameter version [Symbol | String] Version specifier.
# @parameter cache_path [String] Root of the cache directory.
# @returns [Installation]
def self.install(version = :stable, cache_path: Installer.cache_path("chrome"))
Installation.install(version, cache_path: cache_path)
end

# Find an already-installed version or channel without hitting the network.
#
# @parameter version [Symbol | String] Channel or exact version string.
# @parameter cache_path [String] Root of the cache directory.
# @returns [Installation | Nil]
def self.find(version, cache_path: Installer.cache_path("chrome"))
Installation.find(version, Platform.current, cache_path: cache_path)
end
end
end
end
end
Loading
Loading