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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5aad63b2a9668b73cce1b22358cd050e3b126b7c4fde2bc9d998338eacdc6377
4
- data.tar.gz: 73b0d6e9eb31c7ef19cdaa12307c00ba3ff09efb895f9d274f7e46cee552c785
3
+ metadata.gz: 06203afae057f1ba3f11ac14dccfd38dde2698a1d56d5ecbae3961b8317c2f98
4
+ data.tar.gz: 07e491fb4ceec6a760eb0ff2e3f301b8f082edb410dafcf885278aa311c34138
5
5
  SHA512:
6
- metadata.gz: ed9a55e1ad513fe7ae9a9143b97e348dce6c1d9eccc759397ab5c5005439398131085ab5f847e8560e3bd26232bd4486991b4f514e56b6272df2f2406510e422
7
- data.tar.gz: 876dd5764f7582f43f7ffe749f4a5c7aeb032d82abc920d64694804dc39500f23f04c998566fe0d48d4d3cdce1d56e6128a63eeeb3665e33f2f90d9b22ce7422
6
+ metadata.gz: 65e7bafd0d7f1656af4739a55ab37efbb7471d99726cdd38b689e5d4d24f0f3ee9d290cd717ac4c2c39e819953029360b8b6a59b3ed3a7d60bc9610425b068c0
7
+ data.tar.gz: a0baab577a90d809486da2ff3ae12956b07ee11e39131534de2f36e0179ecde07da66f09baffb1e7ed070320ad1520fae4025f1df0b675a4260023af3f84a004
checksums.yaml.gz.sig CHANGED
Binary file
@@ -84,7 +84,7 @@ The most reliable approach is to use `wait_for_navigation` to wait for the URL o
84
84
  ```ruby
85
85
  # ✅ RELIABLE: Wait for URL change
86
86
  session.click_button("Submit")
87
- session.wait_for_navigation {|url| url.end_with?("/success")}
87
+ session.wait_for_navigation{|url| url.end_with?("/success")}
88
88
  session.navigate_to("/next-page") # Now safe
89
89
  ```
90
90
 
@@ -96,7 +96,7 @@ For critical operations like authentication, wait for server-side effects to com
96
96
  # ✅ RELIABLE: Wait for authentication cookie
97
97
  session.click_button("Login")
98
98
  session.wait_for_navigation do |url, ready_state|
99
- ready_state == "complete" && session.cookies.any?{|cookie| cookie['name'] == 'auth_token'}
99
+ ready_state == "complete" && session.cookies.any?{|cookie| cookie["name"] == "auth_token"}
100
100
  end
101
101
  session.navigate_to("/dashboard") # Now safe
102
102
  ```
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "generic"
7
7
  require_relative "process_group"
@@ -20,20 +20,24 @@ module Async
20
20
  # end
21
21
  # ```
22
22
  class Chrome < Generic
23
- def path
24
- @options.fetch(:path, "chromedriver")
23
+ # @returns [String] The path to the `chromedriver` executable.
24
+ def driver_path
25
+ @options.fetch(:driver_path, "chromedriver")
25
26
  end
26
27
 
27
28
  # @returns [String] The version of the `chromedriver` executable.
28
29
  def version
29
- ::IO.popen([self.path, "--version"]) do |io|
30
+ ::IO.popen([self.driver_path, "--version"]) do |io|
30
31
  return io.read
31
32
  end
32
33
  rescue Errno::ENOENT
33
34
  return nil
34
35
  end
35
36
 
37
+ # A locally managed `chromedriver` process.
36
38
  class Driver < Bridge::Driver
39
+ # Initialize a managed Chrome driver process.
40
+ # @parameter options [Hash] Driver configuration options.
37
41
  def initialize(**options)
38
42
  super(**options)
39
43
  @process_group = nil
@@ -42,17 +46,19 @@ module Async
42
46
  # @returns [Array(String)] The arguments to pass to the `chromedriver` executable.
43
47
  def arguments(**options)
44
48
  [
45
- options.fetch(:path, "chromedriver"),
49
+ options.fetch(:driver_path, "chromedriver"),
46
50
  "--port=#{self.port}",
47
51
  ].compact
48
52
  end
49
53
 
54
+ # Start the managed Chrome driver process and wait for readiness.
50
55
  def start
51
56
  @process_group = ProcessGroup.spawn(*arguments(**@options))
52
57
 
53
58
  super
54
59
  end
55
60
 
61
+ # Stop the managed Chrome driver process.
56
62
  def close
57
63
  if @process_group
58
64
  @process_group.close
@@ -63,21 +69,51 @@ module Async
63
69
  end
64
70
  end
65
71
 
66
- # Start the driver.
72
+ # Start the driver, forwarding the bridge's own options to the driver process
73
+ # so that a custom `:driver_path` reaches the chromedriver executable.
67
74
  def start(**options)
68
- Driver.new(**options).tap(&:start)
75
+ Driver.new(**@options, **options).tap(&:start)
76
+ end
77
+
78
+ # Ensure the given version of Chrome for Testing is installed and return a
79
+ # fully configured {Chrome} bridge pointing at it.
80
+ #
81
+ # Delegates to {Async::WebDriver::Installer::Chrome.install} for version
82
+ # resolution and download, then wraps the result in a configured bridge.
83
+ #
84
+ # @parameter version [Symbol | String] `:stable`, `:beta`, `:dev`, `:canary`,
85
+ # a major version string like `"148"`, or an exact version like `"148.0.7778.56"`.
86
+ # @parameter cache_path [String] Root of the cache directory.
87
+ # Default: `~/.cache/async-webdriver.rb` (XDG-compliant).
88
+ # @parameter options [Hash] Additional options forwarded to {.new} (e.g. `headless: false`).
89
+ # @returns [Chrome] A configured bridge.
90
+ def self.for(version = :stable, cache_path: Installer.cache_path("chrome"), **options)
91
+ require_relative "../installer/chrome"
92
+ installation = Installer::Chrome.find(version, cache_path: cache_path) || Installer::Chrome.install(version, cache_path: cache_path)
93
+ new(driver_path: installation.driver_path, browser_path: installation.browser_path, **options)
94
+ end
95
+
96
+ # The path to the Chrome browser executable. If `nil`, ChromeDriver uses its own discovery.
97
+ # @returns [String | Nil]
98
+ def browser_path
99
+ @options[:browser_path]
69
100
  end
70
101
 
71
102
  # The default capabilities for the Chrome browser which need to be provided when requesting a new session.
72
103
  # @parameter headless [Boolean] Whether to run the browser in headless mode.
104
+ # @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.
73
105
  # @returns [Hash] The default capabilities for the Chrome browser.
74
- def default_capabilities(headless: self.headless?)
106
+ def default_capabilities(headless: self.headless?, browser_path: self.browser_path)
107
+ chrome_options = {
108
+ args: [headless ? "--headless=new" : nil].compact,
109
+ }
110
+
111
+ chrome_options[:binary] = browser_path if browser_path
112
+
75
113
  {
76
114
  alwaysMatch: {
77
115
  browserName: "chrome",
78
- "goog:chromeOptions": {
79
- args: [headless ? "--headless" : nil].compact,
80
- },
116
+ "goog:chromeOptions": chrome_options,
81
117
  webSocketUrl: true,
82
118
  },
83
119
  }
@@ -1,19 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module WebDriver
8
8
  module Bridge
9
9
  # Represents an instance of a locally running driver (usually with a process group).
10
10
  class Driver
11
+ # Initialize a driver wrapper.
12
+ # @parameter options [Hash] Driver configuration options.
11
13
  def initialize(**options)
12
14
  @options = options
13
15
  @count = 0
14
16
  @closed = false
15
17
  end
16
18
 
19
+ # @returns [Integer] The number of concurrent sessions the driver can sustain.
17
20
  def concurrency
18
21
  @options.fetch(:concurrency, 128)
19
22
  end
@@ -23,18 +26,22 @@ module Async
23
26
  # @attribute [Hash] The status of the driver after a connection has been established.
24
27
  attr :status
25
28
 
29
+ # @returns [Boolean] Whether the driver can still be used.
26
30
  def viable?
27
31
  !@closed
28
32
  end
29
33
 
34
+ # @returns [Boolean] Whether the driver has been closed.
30
35
  def closed?
31
36
  @closed
32
37
  end
33
38
 
39
+ # Mark the driver as closed.
34
40
  def close
35
41
  @closed = true
36
42
  end
37
43
 
44
+ # @returns [Boolean] Whether the driver may be returned to a pool.
38
45
  def reusable?
39
46
  @options.fetch(:reusable, !@closed)
40
47
  end
@@ -50,14 +57,17 @@ module Async
50
57
  end
51
58
  end
52
59
 
60
+ # @returns [Integer] The port the driver listens on.
53
61
  def port
54
62
  @port ||= @options.fetch(:port, self.ephemeral_port)
55
63
  end
56
64
 
65
+ # @returns [Async::HTTP::Endpoint] The HTTP endpoint exposed by the driver.
57
66
  def endpoint
58
67
  Async::HTTP::Endpoint.parse("http://localhost", port: self.port)
59
68
  end
60
69
 
70
+ # @returns [Client] A client connected to the driver endpoint.
61
71
  def client
62
72
  Client.open(self.endpoint)
63
73
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "generic"
7
7
  require_relative "process_group"
@@ -19,43 +19,50 @@ module Async
19
19
  # bridge&.close
20
20
  # end
21
21
  class Firefox < Generic
22
- def path
23
- @options.fetch(:path, "geckodriver")
22
+ # @returns [String] The path to the `geckodriver` executable.
23
+ def driver_path
24
+ @options.fetch(:driver_path, "geckodriver")
24
25
  end
25
26
 
26
27
  # @returns [String] The version of the `geckodriver` executable.
27
28
  def version
28
- ::IO.popen([self.path, "--version"]) do |io|
29
+ ::IO.popen([self.driver_path, "--version"]) do |io|
29
30
  return io.read
30
31
  end
31
32
  rescue Errno::ENOENT
32
33
  return nil
33
34
  end
34
35
 
36
+ # A locally managed `geckodriver` process.
35
37
  class Driver < Bridge::Driver
38
+ # Initialize a managed Firefox driver process.
39
+ # @parameter options [Hash] Driver configuration options.
36
40
  def initialize(**options)
37
41
  super(**options)
38
42
  @process_group = nil
39
43
  end
40
44
 
45
+ # @returns [Integer] Firefox drivers support one session at a time.
41
46
  def concurrency
42
47
  1
43
48
  end
44
49
 
45
- # @returns [Array(String)] The arguments to pass to the `chromedriver` executable.
50
+ # @returns [Array(String)] The arguments to pass to the `geckodriver` executable.
46
51
  def arguments(**options)
47
52
  [
48
- options.fetch(:path, "geckodriver"),
53
+ options.fetch(:driver_path, "geckodriver"),
49
54
  "--port", self.port.to_s,
50
55
  ].compact
51
56
  end
52
57
 
58
+ # Start the managed Firefox driver process and wait for readiness.
53
59
  def start
54
60
  @process_group = ProcessGroup.spawn(*arguments(**@options))
55
61
 
56
62
  super
57
63
  end
58
64
 
65
+ # Stop the managed Firefox driver process.
59
66
  def close
60
67
  if @process_group
61
68
  @process_group.close
@@ -68,7 +75,7 @@ module Async
68
75
 
69
76
  # Start the driver.
70
77
  def start(**options)
71
- Driver.new(**options).tap(&:start)
78
+ Driver.new(**@options, **options).tap(&:start)
72
79
  end
73
80
 
74
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, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require "socket"
7
7
  require "async/http/endpoint"
@@ -12,6 +12,8 @@ module Async
12
12
  module Bridge
13
13
  # Generic W3C WebDriver implementation.
14
14
  class Generic
15
+ # Initialize a generic bridge wrapper.
16
+ # @parameter options [Hash] Bridge configuration options.
15
17
  def initialize(**options)
16
18
  @options = options
17
19
  end
@@ -26,6 +28,7 @@ module Async
26
28
  version != nil
27
29
  end
28
30
 
31
+ # @returns [Boolean] Whether headless mode is enabled by default.
29
32
  def headless?
30
33
  @options.fetch(:headless, true)
31
34
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require "async/actor"
7
7
  require "async/pool"
@@ -24,14 +24,22 @@ module Async
24
24
  # end
25
25
  # ```
26
26
  class Pool
27
+ # Controls pooled drivers and cached sessions.
27
28
  class BridgeController
29
+ # Initialize the bridge controller.
30
+ # @parameter bridge [Bridge] The bridge used to create drivers.
31
+ # @parameter capabilities [Hash] Capabilities used for new sessions.
28
32
  def initialize(bridge, capabilities: bridge.default_capabilities)
29
33
  @bridge = bridge
30
34
  @capabilities = capabilities
31
35
  @pool = Async::Pool::Controller.new(self)
32
36
  end
33
37
 
38
+ # Caches sessions created from a single driver instance.
34
39
  class SessionCache
40
+ # Initialize a session cache for one driver instance.
41
+ # @parameter driver [Driver] The driver backing cached sessions.
42
+ # @parameter capabilities [Hash] Capabilities for newly created sessions.
35
43
  def initialize(driver, capabilities)
36
44
  @driver = driver
37
45
  @capabilities = capabilities
@@ -40,14 +48,17 @@ module Async
40
48
  @sessions = []
41
49
  end
42
50
 
51
+ # @returns [Boolean] Whether the underlying driver remains usable.
43
52
  def viable?
44
53
  @driver&.viable?
45
54
  end
46
55
 
56
+ # @returns [Boolean] Whether cached sessions may be reused.
47
57
  def reusable?
48
58
  @driver&.reusable?
49
59
  end
50
60
 
61
+ # Close the cached sessions, driver, and HTTP client.
51
62
  def close
52
63
  if @driver
53
64
  @driver.close
@@ -64,10 +75,13 @@ module Async
64
75
  end
65
76
  end
66
77
 
78
+ # @returns [Integer] The number of concurrently usable sessions.
67
79
  def concurrency
68
80
  @driver.concurrency
69
81
  end
70
82
 
83
+ # Acquire a cached or newly created session payload.
84
+ # @returns [Hash] A WebDriver session payload.
71
85
  def acquire
72
86
  if @sessions.empty?
73
87
  session = @client.post("session", {capabilities: @capabilities})
@@ -85,6 +99,8 @@ module Async
85
99
  end
86
100
  end
87
101
 
102
+ # Return a session payload to the cache.
103
+ # @parameter session [Hash] The session payload to cache.
88
104
  def release(session)
89
105
  @sessions.push(session)
90
106
  end
@@ -95,12 +111,16 @@ module Async
95
111
  SessionCache.new(@bridge.start, @capabilities)
96
112
  end
97
113
 
114
+ # Acquire a session payload from the pool.
115
+ # @returns [Hash] The acquired session payload.
98
116
  def acquire
99
117
  session_cache = @pool.acquire
100
118
 
101
119
  return session_cache.acquire
102
120
  end
103
121
 
122
+ # Return a session payload to the pool.
123
+ # @parameter session [Hash] The session payload to release.
104
124
  def release(session)
105
125
  session_cache = session[:cache]
106
126
 
@@ -109,6 +129,8 @@ module Async
109
129
  @pool.release(session_cache)
110
130
  end
111
131
 
132
+ # Retire a session payload and its cache from the pool.
133
+ # @parameter session [Hash] The session payload to retire.
112
134
  def retire(session)
113
135
  session_cache = session[:cache]
114
136
 
@@ -117,6 +139,7 @@ module Async
117
139
  @pool.retire(session_cache)
118
140
  end
119
141
 
142
+ # Close the underlying driver pool.
120
143
  def close
121
144
  if @pool
122
145
  @pool.close
@@ -136,15 +159,19 @@ module Async
136
159
  @controller.close
137
160
  end
138
161
 
162
+ # A pooled session wrapper that returns sessions to the cache on close.
139
163
  class CachedWrapper < Session
164
+ # @returns [Pool] The pool responsible for reusing this session.
140
165
  def pool
141
166
  @options[:pool]
142
167
  end
143
168
 
169
+ # @returns [Hash] The raw session payload returned by the bridge.
144
170
  def payload
145
171
  @options[:payload]
146
172
  end
147
173
 
174
+ # Return the session to the pool when possible.
148
175
  def close
149
176
  unless self.pool.reuse(self)
150
177
  super
@@ -167,6 +194,9 @@ module Async
167
194
  end
168
195
  end
169
196
 
197
+ # Reset and return a session to the pool.
198
+ # @parameter session [CachedWrapper] The session to reuse.
199
+ # @returns [Boolean] Always returns `true` once the session is released.
170
200
  def reuse(session)
171
201
  session.reset!
172
202
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "driver"
7
7
 
@@ -75,13 +75,18 @@ module Async
75
75
  end
76
76
  end
77
77
 
78
+ # A driver wrapper that closes an associated process handle.
78
79
  class ProcessDriver < Driver
80
+ # Initialize a process-backed driver.
81
+ # @parameter endpoint [Object] Driver options or endpoint information.
82
+ # @parameter process [ProcessGroup] The managed process group.
79
83
  def initialize(endpoint, process)
80
84
  super(endpoint)
81
85
 
82
86
  @process = process
83
87
  end
84
88
 
89
+ # Close the driver and its process group.
85
90
  def close
86
91
  super
87
92
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "generic"
7
7
  require_relative "process_group"
@@ -20,20 +20,24 @@ module Async
20
20
  # end
21
21
  # ```
22
22
  class Safari < Generic
23
- def path
24
- @options.fetch(:path, "safaridriver")
23
+ # @returns [String] The path to the `safaridriver` executable.
24
+ def driver_path
25
+ @options.fetch(:driver_path, "safaridriver")
25
26
  end
26
27
 
27
28
  # @returns [String] The version of the `safaridriver` executable.
28
29
  def version
29
- ::IO.popen([self.path, "--version"]) do |io|
30
+ ::IO.popen([self.driver_path, "--version"]) do |io|
30
31
  return io.read
31
32
  end
32
33
  rescue Errno::ENOENT
33
34
  return nil
34
35
  end
35
36
 
37
+ # A locally managed `safaridriver` process.
36
38
  class Driver < Bridge::Driver
39
+ # Initialize a managed Safari driver process.
40
+ # @parameter options [Hash] Driver configuration options.
37
41
  def initialize(**options)
38
42
  super(**options)
39
43
  @process_group = nil
@@ -42,17 +46,19 @@ module Async
42
46
  # @returns [Array(String)] The arguments to pass to the `safaridriver` executable.
43
47
  def arguments(**options)
44
48
  [
45
- options.fetch(:path, "safaridriver"),
49
+ options.fetch(:driver_path, "safaridriver"),
46
50
  "--port=#{self.port}",
47
51
  ].compact
48
52
  end
49
53
 
54
+ # Start the managed Safari driver process and wait for readiness.
50
55
  def start
51
56
  @process_group = ProcessGroup.spawn(*arguments(**@options))
52
57
 
53
58
  super
54
59
  end
55
60
 
61
+ # Stop the managed Safari driver process.
56
62
  def close
57
63
  if @process_group
58
64
  @process_group.close
@@ -65,7 +71,7 @@ module Async
65
71
 
66
72
  # Start the driver.
67
73
  def start(**options)
68
- Driver.new(**options).tap(&:start)
74
+ Driver.new(**@options, **options).tap(&:start)
69
75
  end
70
76
 
71
77
  # The default capabilities for the Safari 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, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "bridge/chrome"
7
7
  require_relative "bridge/firefox"
@@ -21,6 +21,9 @@ module Async
21
21
  Bridge::Safari,
22
22
  ]
23
23
 
24
+ # Iterate over supported bridge implementations.
25
+ # @yields {|bridge| ...} Each supported bridge class.
26
+ # @parameter bridge [Class] A supported bridge implementation.
24
27
  def self.each(&block)
25
28
  return enum_for(:each) unless block_given?
26
29
 
@@ -45,6 +48,7 @@ module Async
45
48
  # ```
46
49
  ASYNC_WEBDRIVER_BRIDGE_HEADLESS = "ASYNC_WEBDRIVER_BRIDGE_HEADLESS"
47
50
 
51
+ # Raised when no supported bridge implementation is available.
48
52
  class UnsupportedError < Error
49
53
  end
50
54
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "request_helper"
7
7
 
@@ -20,6 +20,8 @@ module Async
20
20
  class Attributes
21
21
  include Enumerable
22
22
 
23
+ # Initialize the attribute wrapper for an element.
24
+ # @parameter element [Element] The element whose attributes will be accessed.
23
25
  def initialize(element)
24
26
  @element = element
25
27
  @keys = nil
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "version"
7
7
 
8
8
  module Async
9
9
  module WebDriver
10
+ # The base class for WebDriver protocol errors.
10
11
  class Error < StandardError
11
12
  end
12
13