async-webdriver 0.9.1 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/index.yaml +4 -0
- data/context/navigation-timing.md +157 -0
- data/lib/async/webdriver/scope/navigation.rb +33 -0
- data/lib/async/webdriver/version.rb +1 -1
- data/readme.md +6 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +2 -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: 5aad63b2a9668b73cce1b22358cd050e3b126b7c4fde2bc9d998338eacdc6377
|
4
|
+
data.tar.gz: 73b0d6e9eb31c7ef19cdaa12307c00ba3ff09efb895f9d274f7e46cee552c785
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed9a55e1ad513fe7ae9a9143b97e348dce6c1d9eccc759397ab5c5005439398131085ab5f847e8560e3bd26232bd4486991b4f514e56b6272df2f2406510e422
|
7
|
+
data.tar.gz: 876dd5764f7582f43f7ffe749f4a5c7aeb032d82abc920d64694804dc39500f23f04c998566fe0d48d4d3cdce1d56e6128a63eeeb3665e33f2f90d9b22ce7422
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/context/index.yaml
CHANGED
@@ -15,6 +15,10 @@ files:
|
|
15
15
|
title: Debugging
|
16
16
|
description: This guide explains how to debug WebDriver issues by capturing HTML
|
17
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.
|
18
22
|
- path: github-actions-integration.md
|
19
23
|
title: GitHub Actions Integrations
|
20
24
|
description: This guide explains how to use `async-webdriver` with GitHub Actions.
|
@@ -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.
|
@@ -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
|
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
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.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -116,6 +116,7 @@ files:
|
|
116
116
|
- context/getting-started.md
|
117
117
|
- context/github-actions-integration.md
|
118
118
|
- context/index.yaml
|
119
|
+
- context/navigation-timing.md
|
119
120
|
- context/sus-integration.md
|
120
121
|
- lib/async/webdriver.rb
|
121
122
|
- lib/async/webdriver/bridge.rb
|
metadata.gz.sig
CHANGED
Binary file
|