async-webdriver 0.9.0 → 0.10.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: fe93050116a6c87ff0cdb48231966242d1c99e2bab1b773f455ff2d0fdda5a87
4
- data.tar.gz: 25977c570eb1235fadf1eee31a2e42c1fc97f98b7ccd9759aea46959b6fd82ed
3
+ metadata.gz: 5aad63b2a9668b73cce1b22358cd050e3b126b7c4fde2bc9d998338eacdc6377
4
+ data.tar.gz: 73b0d6e9eb31c7ef19cdaa12307c00ba3ff09efb895f9d274f7e46cee552c785
5
5
  SHA512:
6
- metadata.gz: 5a433e7bd71265ef9e65097cdac2d19b966ac3c7ddabf2cbe02b64ffc17367fcfb9218aac5cfcd230ed481c819b0bed34ed91c8238e445cc426c6b219a64e5f8
7
- data.tar.gz: 8e762c8be4bf92aab638d38c047d8644a29a9e856256717eb6153ff6b8e1efc563f36fe6267ad6a8a2bf0a9a4c1521de1387249f00fab74da6987287609dbd16
6
+ metadata.gz: ed9a55e1ad513fe7ae9a9143b97e348dce6c1d9eccc759397ab5c5005439398131085ab5f847e8560e3bd26232bd4486991b4f514e56b6272df2f2406510e422
7
+ data.tar.gz: 876dd5764f7582f43f7ffe749f4a5c7aeb032d82abc920d64694804dc39500f23f04c998566fe0d48d4d3cdce1d56e6128a63eeeb3665e33f2f90d9b22ce7422
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,252 @@
1
+ # Debugging
2
+
3
+ This guide explains how to debug WebDriver issues by capturing HTML source and screenshots when tests fail.
4
+
5
+ ## Overview
6
+
7
+ When WebDriver tests fail, it's often helpful to capture the current state of the page to understand what went wrong. The most useful debugging artifacts are:
8
+
9
+ - **HTML Source**: Shows the current DOM structure, helpful for understanding why element selectors might be failing
10
+ - **Screenshots**: Provides a visual representation of what the browser is actually showing
11
+
12
+ ## Core Concepts
13
+
14
+ `async-webdriver` provides built-in methods for capturing debugging information:
15
+
16
+ - {ruby Async::WebDriver::Session#document_source} returns the HTML source of the current page.
17
+ - {ruby Async::WebDriver::Session#screenshot} captures a screenshot of the entire page.
18
+ - {ruby Async::WebDriver::Element#screenshot} captures a screenshot of a specific element.
19
+
20
+ ## Basic Debugging
21
+
22
+ ### Capturing HTML Source
23
+
24
+ To save the current page HTML to a file:
25
+
26
+ ```ruby
27
+ require "async/webdriver"
28
+
29
+ Async::WebDriver::Bridge::Pool.open do |pool|
30
+ pool.session do |session|
31
+ session.visit("https://example.com")
32
+
33
+ # Save HTML source for debugging
34
+ html = session.document_source
35
+ File.write("debug.html", html)
36
+
37
+ puts "HTML saved to debug.html"
38
+ end
39
+ end
40
+ ```
41
+
42
+ ### Capturing Screenshots
43
+
44
+ To save a screenshot of the current page:
45
+
46
+ ```ruby
47
+ require "async/webdriver"
48
+
49
+ Async::WebDriver::Bridge::Pool.open do |pool|
50
+ pool.session do |session|
51
+ session.visit("https://example.com")
52
+
53
+ # Take a screenshot (returns PNG binary data)
54
+ screenshot_data = session.screenshot
55
+ File.binwrite("debug.png", screenshot_data)
56
+
57
+ puts "Screenshot saved to debug.png"
58
+ end
59
+ end
60
+ ```
61
+
62
+ ### Element Screenshots
63
+
64
+ To capture a screenshot of a specific element:
65
+
66
+ ```ruby
67
+ require "async/webdriver"
68
+
69
+ Async::WebDriver::Bridge::Pool.open do |pool|
70
+ pool.session do |session|
71
+ session.visit("https://example.com")
72
+
73
+ # Find an element and screenshot it
74
+ element = session.find_element_by_tag_name("body")
75
+ element_screenshot = element.screenshot
76
+ File.binwrite("element-debug.png", element_screenshot)
77
+
78
+ puts "Element screenshot saved to element-debug.png"
79
+ end
80
+ end
81
+ ```
82
+
83
+ ## Debugging Failed Element Searches
84
+
85
+ A common debugging scenario is when `find_element` fails. Here's how to capture debugging information:
86
+
87
+ ```ruby
88
+ require "async/webdriver"
89
+
90
+ def debug_element_search(session, locator_type, locator_value)
91
+ begin
92
+ # Use the correct locator format for async-webdriver
93
+ locator = {using: locator_type, value: locator_value}
94
+ element = session.find_element(locator)
95
+ puts "✅ Element found: #{locator_type}=#{locator_value}"
96
+ return element
97
+ rescue Async::WebDriver::NoSuchElementError => e
98
+ puts "❌ Element not found: #{locator_type}=#{locator_value}"
99
+
100
+ # Capture debugging information
101
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
102
+
103
+ # Save HTML source
104
+ html = session.document_source
105
+ html_file = "debug-#{timestamp}.html"
106
+ File.write(html_file, html)
107
+ puts "📄 HTML saved to #{html_file}"
108
+
109
+ # Save screenshot
110
+ screenshot_data = session.screenshot
111
+ screenshot_file = "debug-#{timestamp}.png"
112
+ File.binwrite(screenshot_file, screenshot_data)
113
+ puts "📸 Screenshot saved to #{screenshot_file}"
114
+
115
+ # Re-raise the original error
116
+ raise e
117
+ end
118
+ end
119
+
120
+ # Usage example
121
+ Async::WebDriver::Bridge::Pool.open do |pool|
122
+ pool.session do |session|
123
+ session.visit("https://example.com")
124
+
125
+ # This will save debug files if the element isn't found
126
+ button = debug_element_search(session, "id", "submit-button")
127
+ end
128
+ end
129
+ ```
130
+
131
+ ## Advanced Debugging Techniques
132
+
133
+ ### Configuring Timeouts for Debugging
134
+
135
+ WebDriver uses different timeout settings that affect how long operations wait:
136
+
137
+ ```ruby
138
+ require "async/webdriver"
139
+
140
+ Async::WebDriver::Bridge::Pool.open do |pool|
141
+ pool.session do |session|
142
+ # Configure timeouts for debugging (values in milliseconds)
143
+ session.implicit_wait_timeout = 10_000 # 10 seconds for element finding
144
+ session.page_load_timeout = 30_000 # 30 seconds for page loads
145
+ session.script_timeout = 5_000 # 5 seconds for JavaScript execution
146
+
147
+ puts "Current timeouts: #{session.timeouts}"
148
+
149
+ # Now element finding will wait up to 10 seconds
150
+ session.visit("https://example.com")
151
+ element = session.find_element(:id, "dynamic-content") # Will wait up to 10s
152
+ end
153
+ end
154
+ ```
155
+
156
+ ### Wait and Debug Pattern
157
+
158
+ Sometimes elements appear after a delay. Here's how to debug timing issues:
159
+
160
+ ```ruby
161
+ require "async/webdriver"
162
+
163
+ def wait_and_debug(session, locator_type, locator_value, timeout: 10000)
164
+ # Set implicit wait timeout (in milliseconds)
165
+ original_timeout = session.implicit_wait_timeout
166
+ session.implicit_wait_timeout = timeout
167
+
168
+ start_time = Time.now
169
+
170
+ begin
171
+ # Try to find the element (will use implicit wait timeout)
172
+ locator = {using: locator_type, value: locator_value}
173
+ session.find_element(locator)
174
+ rescue Async::WebDriver::NoSuchElementError => error
175
+ elapsed = Time.now - start_time
176
+ puts "⏰ Timeout after #{elapsed.round(2)}s waiting for #{locator_type}=#{locator_value}"
177
+
178
+ # Capture final state
179
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
180
+
181
+ html = session.document_source
182
+ File.write("timeout-debug-#{timestamp}.html", html)
183
+
184
+ screenshot_data = session.screenshot
185
+ File.binwrite("timeout-debug-#{timestamp}.png", screenshot_data)
186
+
187
+ puts "📄 Final HTML saved to timeout-debug-#{timestamp}.html"
188
+ puts "📸 Final screenshot saved to timeout-debug-#{timestamp}.png"
189
+
190
+ raise
191
+ ensure
192
+ # Restore original timeout
193
+ session.implicit_wait_timeout = original_timeout
194
+ end
195
+ end
196
+ ```
197
+
198
+ ### Multi-Step Debugging
199
+
200
+ For complex test scenarios, capture state at multiple points:
201
+
202
+ ```ruby
203
+ require "async/webdriver"
204
+
205
+ class DebugHelper
206
+ def initialize(test_name)
207
+ @test_name = test_name
208
+ @step = 0
209
+ end
210
+
211
+ def capture_state(session, description)
212
+ @step += 1
213
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
214
+ prefix = "#{@test_name}-step#{@step}-#{timestamp}"
215
+
216
+ # Save HTML
217
+ html = session.document_source
218
+ html_file = "#{prefix}-#{description}.html"
219
+ File.write(html_file, html)
220
+
221
+ # Save screenshot
222
+ screenshot_data = session.screenshot
223
+ screenshot_file = "#{prefix}-#{description}.png"
224
+ File.binwrite(screenshot_file, screenshot_data)
225
+
226
+ puts "🔍 Step #{@step}: #{description}"
227
+ puts " 📄 #{html_file}"
228
+ puts " 📸 #{screenshot_file}"
229
+ end
230
+ end
231
+
232
+ # Usage example
233
+ debug = DebugHelper.new("login-test")
234
+
235
+ Async::WebDriver::Bridge::Pool.open do |pool|
236
+ pool.session do |session|
237
+ debug.capture_state(session, "initial-page")
238
+
239
+ session.visit("https://example.com/login")
240
+ debug.capture_state(session, "login-page-loaded")
241
+
242
+ session.find_element_by_id("username").send_keys("user@example.com")
243
+ debug.capture_state(session, "username-entered")
244
+
245
+ session.find_element_by_id("password").send_keys("password")
246
+ debug.capture_state(session, "password-entered")
247
+
248
+ session.find_element_by_id("submit").click
249
+ debug.capture_state(session, "form-submitted")
250
+ end
251
+ end
252
+ ```
@@ -0,0 +1,90 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `async-webdriver` for controlling a browser.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add async-webdriver
11
+ ~~~
12
+
13
+ ## Core Concepts
14
+
15
+ `async-webdriver` is a Ruby implementation of the [WebDriver](https://www.w3.org/TR/webdriver/) protocol. It allows you to control a browser from Ruby code. It is built on top of [async](https://github.com/socketry/async) and [async-http](https://github.com/socketry/async-http). It has several core concepts:
16
+
17
+ - A {ruby Async::WebDriver::Bridge} can be used to start a web driver process, e.g. `chromedriver`, `geckodriver`, etc. It can be used in isolation, or not at all.
18
+ - A {ruby Async::WebDriver::Client} is used to connect to a running web driver and can be used to create new sessions.
19
+ - A {ruby Async::WebDriver::Session} represents a single browser session. It is used to control a browser window and navigate to different pages.
20
+ - A {ruby Async::WebDriver::Element} represents a single element on a page. It can be used to interact with the element, e.g. click, type, etc.
21
+
22
+ ## Basic Usage
23
+
24
+ The following example shows how to use `async-webdriver` to open a browser, navigate to a page, and click a button:
25
+
26
+ ~~~ ruby
27
+ require 'async/webdriver'
28
+
29
+ Async do
30
+ bridge = Async::WebDriver::Bridge::Chrome.new(headless: false)
31
+
32
+ driver = bridge.start
33
+ client = Async::WebDriver::Client.open(driver.endpoint)
34
+
35
+ session = client.session(bridge.default_capabilities)
36
+ # Set the implicit wait timeout to 10 seconds since we are dealing with the real internet (which can be slow):
37
+ session.implicit_wait_timeout = 10_000
38
+
39
+ session.visit('https://google.com')
40
+
41
+ session.fill_in('q', 'async-webdriver')
42
+ session.click_button("I'm Feeling Lucky")
43
+
44
+ puts session.document_title
45
+ ensure
46
+ session&.close
47
+ client&.close
48
+ driver&.close
49
+ end
50
+ ~~~
51
+
52
+ ### Using a Pool to Manage Sessions
53
+
54
+ If you are running multiple tests in parallel, you may want to use a session pool to manage the sessions. This can be done as follows:
55
+
56
+ ~~~ ruby
57
+ require 'async/webdriver'
58
+
59
+ Async do
60
+ bridge = Async::WebDriver::Bridge::Pool.new(Async::WebDriver::Bridge::Chrome.new(headless: false))
61
+
62
+ session = bridge.session
63
+ # Set the implicit wait timeout to 10 seconds since we are dealing with the real internet (which can be slow):
64
+ session.implicit_wait_timeout = 10_000
65
+
66
+ session.visit('https://google.com')
67
+
68
+ session.fill_in('q', 'async-webdriver')
69
+ session.click_button("I'm Feeling Lucky")
70
+
71
+ puts session.document_title
72
+ ensure
73
+ session&.close
74
+ bridge&.close
75
+ end
76
+ ~~~
77
+
78
+ The sessions will be cached and reused if possible.
79
+
80
+ ## Integration vs Unit Testing
81
+
82
+ `async-webdriver` is designed for integration testing. It is not designed for unit testing (e.g. wrapping a tool like `rack-test` as `capybara` can do). It is designed for testing your application in a real browser and web server. It is designed for testing your application in the same way that a real user would use it. Unfortunately, this style of integration testing is significantly slower than unit testing, but it is also significantly more representative of how your application will behave in production. There are other tools, e.g. [rack-test](https://github.com/rack/rack-test) which provide significantly faster unit testing, but they do not test how your application will behave in an actual web browser. A comprehensive test suite should include both unit tests and integration tests.
83
+
84
+ ### Headless Mode
85
+
86
+ During testing, often you will want to see the real browser window to determine if the test is working correctly. By default, for performance reasons, `async-webdriver` will run the browser in headless mode. This means that the browser will not be visible on the screen. If you want to see the browser window, you can disable headless mode by setting the `headless` option to `false`:
87
+
88
+ ~~~ shell
89
+ $ ASYNC_WEBDRIVER_BRIDGE_HEADLESS=false ./webdriver-script.rb
90
+ ~~~
@@ -0,0 +1,68 @@
1
+ # GitHub Actions Integrations
2
+
3
+ This guide explains how to use `async-webdriver` with GitHub Actions.
4
+
5
+ We recommend using the [browser-actions](https://github.com/browser-actions) for setting up `chromedriver` and `geckodriver`. They are pre-configured to work with `async-webdriver` and are easy to use.
6
+
7
+ ## Pipeline Configuration
8
+
9
+ The following example shows how to setup both `chromedriver` and `geckodriver` in a single pipeline:
10
+
11
+ ~~~ yaml
12
+ name: Test
13
+
14
+ on: [push, pull_request]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ env:
20
+ CONSOLE_OUTPUT: XTerm
21
+
22
+ jobs:
23
+ test:
24
+ name: ${{matrix.ruby}} on ${{matrix.os}}
25
+ runs-on: ${{matrix.os}}-latest
26
+ continue-on-error: ${{matrix.experimental}}
27
+
28
+ strategy:
29
+ matrix:
30
+ os:
31
+ - ubuntu
32
+ - macos
33
+
34
+ ruby:
35
+ - "3.0"
36
+ - "3.1"
37
+ - "3.2"
38
+
39
+ experimental: [false]
40
+
41
+ include:
42
+ - os: ubuntu
43
+ ruby: truffleruby
44
+ experimental: true
45
+ - os: ubuntu
46
+ ruby: jruby
47
+ experimental: true
48
+ - os: ubuntu
49
+ ruby: head
50
+ experimental: true
51
+
52
+ steps:
53
+ - uses: actions/checkout@v3
54
+ - uses: ruby/setup-ruby@v1
55
+ with:
56
+ ruby-version: ${{matrix.ruby}}
57
+ bundler-cache: true
58
+
59
+ - uses: browser-actions/setup-chrome@v1
60
+ - uses: browser-actions/setup-firefox@v1
61
+ - uses: browser-actions/setup-geckodriver@latest
62
+ with:
63
+ token: ${{secrets.GITHUB_TOKEN}}
64
+
65
+ - name: Run tests
66
+ timeout-minutes: 10
67
+ run: bundle exec bake test
68
+ ~~~
@@ -0,0 +1,28 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A native library implementing the W3C WebDriver client specification.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/async-webdriver/
7
+ funding_uri: https://github.com/sponsors/ioquatix
8
+ source_code_uri: https://github.com/socketry/async-webdriver.git
9
+ files:
10
+ - path: getting-started.md
11
+ title: Getting Started
12
+ description: This guide explains how to use `async-webdriver` for controlling a
13
+ browser.
14
+ - path: debugging.md
15
+ title: Debugging
16
+ description: This guide explains how to debug WebDriver issues by capturing HTML
17
+ source and screenshots when tests fail.
18
+ - path: navigation-timing.md
19
+ title: Navigation Timing
20
+ description: This guide explains how to avoid race conditions when triggering navigation
21
+ operations while browser navigation is already in progress.
22
+ - path: github-actions-integration.md
23
+ title: GitHub Actions Integrations
24
+ description: This guide explains how to use `async-webdriver` with GitHub Actions.
25
+ - path: sus-integration.md
26
+ title: Sus Integration
27
+ description: This guide will show you how to integrate `async-webdriver` with the
28
+ sus test framework.
@@ -0,0 +1,157 @@
1
+ # Navigation Timing
2
+
3
+ This guide explains how to avoid race conditions when triggering navigation operations while browser navigation is already in progress.
4
+
5
+ ## The Problem
6
+
7
+ When you trigger navigation in a browser (form submission, link clicks), the browser starts a complex process that takes time. If you call `navigate_to` while this process is still running, it will interrupt the ongoing navigation, potentially causing:
8
+
9
+ - Server-side effects (like setting session cookies) to not complete.
10
+ - The intended navigation to never finish.
11
+ - Your test to end up in an unexpected state.
12
+
13
+ ## Understanding the Race Condition
14
+
15
+ When you trigger navigation (form submission, link clicks), the browser starts a process:
16
+
17
+ 1. **Submit Request**: Form data is sent to the server.
18
+ 2. **Server Processing**: Server handles the request (authentication, validation, etc.).
19
+ 3. **Response**: Server sends back response (redirect, new page, etc.).
20
+ 4. **Browser Navigation**: Browser processes the response and updates the page.
21
+ 5. **Page Load**: New page loads and `document.readyState` becomes "complete".
22
+
23
+ If any navigation operation is triggered during steps 1-4, it **interrupts** this process:
24
+ - Server-side effects (like setting session cookies) may not complete.
25
+ - The intended navigation never finishes.
26
+ - Your test ends up in an unexpected state.
27
+
28
+ ## The Redirect Race Condition
29
+
30
+ A particularly common variant of this race condition occurs with **HTTP redirects** (302, 301, etc.). When a form submission or other action triggers a redirect:
31
+
32
+ 1. **Form Submission**: Browser sends POST request to `/submit`.
33
+ 2. **Server Response**: Server returns `302 Found` with `Location: /success` header.
34
+ 3. **Redirect Processing**: Browser receives the 302 response (usually with empty body).
35
+ 4. **Follow Redirect**: Browser automatically navigates to `/success`.
36
+ 5. **Final Page Load**: Success page loads with actual content.
37
+
38
+ The race condition occurs because element-based waits can execute during step 3, when the browser has received the 302 response but hasn't yet loaded the target page:
39
+
40
+ ```ruby
41
+ session.click_button("Submit") # Triggers POST -> 302 redirect
42
+ session.find_element(xpath: "//h1") # May execute on empty 302 page!
43
+ session.navigate_to("/other-page") # Interrupts redirect to /success
44
+ ```
45
+
46
+ This explains why redirect-based workflows (login forms, contact forms, checkout processes) are particularly susceptible to race conditions.
47
+
48
+ ## Problematic Code Examples
49
+
50
+ ### Example 1: Login Form Race Condition
51
+
52
+ ```ruby
53
+ # ❌ PROBLEMATIC: May interrupt login before authentication completes
54
+ session.click_button("Login") # Triggers form submission.
55
+ session.navigate_to("/dashboard") # May interrupt login process!
56
+ ```
57
+
58
+ ### Example 2: Form Submission Race Condition
59
+
60
+ ```ruby
61
+ # ❌ PROBLEMATIC: May interrupt form submission
62
+ session.fill_in("email", "user@example.com")
63
+ session.click_button("Subscribe") # Triggers form submission.
64
+ session.navigate_to("/thank-you") # May interrupt subscription action on server!
65
+ ```
66
+
67
+ ### Example 3: Redirect Race Condition
68
+
69
+ ```ruby
70
+ # ❌ PROBLEMATIC: May interrupt redirect before it completes
71
+ session.click_button("Submit") # POST -> 302 redirect.
72
+ session.find_element(xpath: "//h1") # May find element on 302 page and fail.
73
+ session.navigate_to("/dashboard") # Interrupts redirect to success page.
74
+ ```
75
+
76
+ ## Detection and Mitigation Strategies
77
+
78
+ ⚠️ **Important**: Element-based waits (`find_element`) are **insufficient** for preventing race conditions because navigation can be interrupted before target elements ever appear on the page.
79
+
80
+ ### Reliable Strategy: Use `wait_for_navigation`
81
+
82
+ The most reliable approach is to use `wait_for_navigation` to wait for the URL or page state to change:
83
+
84
+ ```ruby
85
+ # ✅ RELIABLE: Wait for URL change
86
+ session.click_button("Submit")
87
+ session.wait_for_navigation {|url| url.end_with?("/success")}
88
+ session.navigate_to("/next-page") # Now safe
89
+ ```
90
+
91
+ ### Alternative: Wait for Server-Side Effects
92
+
93
+ For critical operations like authentication, wait for server-side effects to complete:
94
+
95
+ ```ruby
96
+ # ✅ RELIABLE: Wait for authentication cookie
97
+ session.click_button("Login")
98
+ session.wait_for_navigation do |url, ready_state|
99
+ ready_state == "complete" && session.cookies.any?{|cookie| cookie['name'] == 'auth_token'}
100
+ end
101
+ session.navigate_to("/dashboard") # Now safe
102
+ ```
103
+
104
+ ### Unreliable Approaches (Common But Insufficient)
105
+
106
+ These approaches are commonly used but **may still allow race conditions**:
107
+
108
+ #### Element-based Waits
109
+
110
+ Unfortunately, waiting for specific elements to appear does not always work when navigation operations are in progress. This is especially problematic with redirects, where element waits can execute on the intermediate redirect response (which typically has no content) rather than the final destination page.
111
+
112
+ ```ruby
113
+ # ❌ UNRELIABLE: Navigation can be interrupted before element appears
114
+ session.click_button("Submit") # Triggers POST -> 302 redirect
115
+
116
+ # In principle, wait for the form submission to complete:
117
+ session.find_element(xpath: "//h1[text()='Success']")
118
+ # However, in reality it may:
119
+ # 1. Execute on the 302 redirect page (empty content) and fail immediately
120
+ # 2. Hang if the redirect navigation is still in progress
121
+ # 3. Succeed by chance if the redirect has completed sufficiently
122
+
123
+ # Assuming the previous operation did not hang, this navigation may interrupt the redirect:
124
+ session.navigate_to("/next-page")
125
+ ```
126
+
127
+ #### Generic Page Waits
128
+
129
+ ```ruby
130
+ # ❌ UNRELIABLE: Doesn't ensure the intended navigation completed
131
+ session.click_button("Submit")
132
+
133
+ # This can find the wrong element on the initial page before the form submission causes a page navigation operation:
134
+ session.find_element(xpath: "//html")
135
+
136
+ # This navigation may interrupt the form submission:
137
+ session.navigate_to("/next-page")
138
+ ```
139
+
140
+ These approaches fail because `navigate_to` can interrupt the ongoing navigation before the target page (and its elements) ever loads.
141
+
142
+ ## Best Practices Summary
143
+
144
+ 1. **Always wait** after triggering navigation before calling `navigate_to` again.
145
+ 2. **Use `wait_for_navigation`** with URL or state conditions for reliable synchronization.
146
+ 3. **Test for race conditions** in your test suite with deliberate delays.
147
+ 4. **Avoid element-based waits** for navigation synchronization (they're unreliable).
148
+ 5. **Consider server-side effects** when designing wait conditions.
149
+ 6. **Prefer URL-based waits** over element-based waits for navigation timing.
150
+
151
+ ## Common Pitfalls
152
+
153
+ - **Don't assume** `click_button` waits for navigation to complete.
154
+ - **Don't rely on** element-based waits (`find_element`) to prevent race conditions.
155
+ - **Don't use** arbitrary `sleep` calls instead of proper synchronization.
156
+ - **Don't ignore** server-side effects like cookie setting or session management.
157
+ - **Don't chain** multiple navigation operations without URL-based synchronization.
@@ -0,0 +1,51 @@
1
+ # Sus Integration
2
+
3
+ This guide will show you how to integrate `async-webdriver` with the sus test framework.
4
+
5
+ ## Usage
6
+
7
+ Sus has out of the box support for `async-webdriver`. You can use it like this:
8
+
9
+ ```shell
10
+ $ bundle add sus-fixtures-async-http sus-fixtures-async-webdriver protocol-rack
11
+ $ bundle update
12
+ ```
13
+
14
+ Then write your integration test:
15
+
16
+ ```ruby
17
+ # test/my_integration_test.rb
18
+
19
+ require "sus/fixtures/async/http/server_context"
20
+ require "sus/fixtures/async/webdriver/session_context"
21
+
22
+ require "protocol/rack/adapter"
23
+ require "rack/builder"
24
+
25
+ describe "my website" do
26
+ include Sus::Fixtures::Async::HTTP::ServerContext
27
+ include Sus::Fixtures::Async::WebDriver::SessionContext
28
+
29
+ def middleware
30
+ Protocol::Rack::Adapter.new(app)
31
+ end
32
+
33
+ def app
34
+ Rack::Builder.load_file(File.expand_path("../config.ru", __dir__))
35
+ end
36
+
37
+ it "has a title" do
38
+ navigate_to("/")
39
+
40
+ expect(session.document_title).to be == "Example"
41
+ end
42
+
43
+ it "has a paragraph" do
44
+ navigate_to("/")
45
+
46
+ expect(session).to have_element(tag_name: "p")
47
+ end
48
+ end
49
+ ```
50
+
51
+ For more information, refer to the [sus-fixtures-async-webdriver](https://github.com/socketry/sus-fixtures-async-webdriver) documentation.
@@ -4,11 +4,14 @@
4
4
  # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
6
  require "uri"
7
+ require "async/clock"
7
8
 
8
9
  module Async
9
10
  module WebDriver
10
11
  module Scope
11
12
  # Helpers for navigating the browser.
13
+ #
14
+ # ⚠️ **Important**: Navigation operations (and events that trigger navigation) may result in race conditions if not properly synchronized. Consult the "Navigation Timing" Guide in the documentation for more details.
12
15
  module Navigation
13
16
  # Navigate to the given URL.
14
17
  # @parameter url [String] The URL to navigate to.
@@ -44,6 +47,36 @@ module Async
44
47
  def refresh
45
48
  session.post("refresh")
46
49
  end
50
+
51
+ # Wait for navigation to complete with custom conditions.
52
+ #
53
+ # This method helps avoid race conditions by polling the browser state until your specified conditions are met.
54
+ #
55
+ # @parameter timeout [Float] Maximum time to wait in seconds (default: 10.0).
56
+ # @yields {|current_url| ...} Yields the current URL to the block, when the ready state is "complete".
57
+ # @yields {|current_url, ready_state| ...} Yields both the current URL and ready state to the block, allowing more complex conditions.
58
+ def wait_for_navigation(timeout: 10.0, &block)
59
+ clock = Clock.start
60
+ duration = [timeout / 100.0, 0.005].max
61
+
62
+ while true
63
+ current_url = session.current_url
64
+ ready_state = session.execute("return document.readyState;")
65
+
66
+ if block.arity > 1
67
+ break if yield(current_url, ready_state)
68
+ else
69
+ break if ready_state == "complete" && yield(current_url)
70
+ end
71
+
72
+ if clock.total > timeout
73
+ raise TimeoutError, "Timed out waiting for navigation to complete (current_url: #{current_url}, ready_state: #{ready_state})"
74
+ end
75
+
76
+ Console.debug(self, "Waiting for navigation...", ready_state: ready_state, location: current_url, elapsed: clock.total)
77
+ sleep(duration)
78
+ end
79
+ end
47
80
  end
48
81
  end
49
82
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module WebDriver
8
- VERSION = "0.9.0"
8
+ VERSION = "0.10.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -18,6 +18,8 @@ Please see the [project documentation](https://socketry.github.io/async-webdrive
18
18
 
19
19
  - [Debugging](https://socketry.github.io/async-webdriver/guides/debugging/index) - This guide explains how to debug WebDriver issues by capturing HTML source and screenshots when tests fail.
20
20
 
21
+ - [Navigation Timing](https://socketry.github.io/async-webdriver/guides/navigation-timing/index) - This guide explains how to avoid race conditions when triggering navigation operations while browser navigation is already in progress.
22
+
21
23
  - [GitHub Actions Integrations](https://socketry.github.io/async-webdriver/guides/github-actions-integration/index) - This guide explains how to use `async-webdriver` with GitHub Actions.
22
24
 
23
25
  - [Sus Integration](https://socketry.github.io/async-webdriver/guides/sus-integration/index) - This guide will show you how to integrate `async-webdriver` with the sus test framework.
@@ -26,6 +28,10 @@ Please see the [project documentation](https://socketry.github.io/async-webdrive
26
28
 
27
29
  Please see the [project releases](https://socketry.github.io/async-webdriver/releases/index) for all releases.
28
30
 
31
+ ### v0.10.0
32
+
33
+ - Introduce `Scope#wait_for_navigation` to properly wait for page navigations to complete.
34
+
29
35
  ### v0.9.0
30
36
 
31
37
  - Fix `Scope#screenshot` to use the correct HTTP method (`GET` instead of `POST`).
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.10.0
4
+
5
+ - Introduce `Scope#wait_for_navigation` to properly wait for page navigations to complete.
6
+
3
7
  ## v0.9.0
4
8
 
5
9
  - Fix `Scope#screenshot` to use the correct HTTP method (`GET` instead of `POST`).
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.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -112,6 +112,12 @@ executables: []
112
112
  extensions: []
113
113
  extra_rdoc_files: []
114
114
  files:
115
+ - context/debugging.md
116
+ - context/getting-started.md
117
+ - context/github-actions-integration.md
118
+ - context/index.yaml
119
+ - context/navigation-timing.md
120
+ - context/sus-integration.md
115
121
  - lib/async/webdriver.rb
116
122
  - lib/async/webdriver/bridge.rb
117
123
  - lib/async/webdriver/bridge/chrome.rb
metadata.gz.sig CHANGED
Binary file