capybara-lockstep 1.3.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +1 -3
- data/.gitignore +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +6 -8
- data/Gemfile.lock +38 -34
- data/README.md +68 -27
- data/Rakefile +0 -2
- data/capybara-lockstep.gemspec +3 -3
- data/lib/capybara-lockstep/capybara_ext.rb +82 -35
- data/lib/capybara-lockstep/client.rb +133 -0
- data/lib/capybara-lockstep/configuration.rb +16 -6
- data/lib/capybara-lockstep/helper.js +30 -13
- data/lib/capybara-lockstep/lockstep.rb +35 -140
- data/lib/capybara-lockstep/logging.rb +6 -6
- data/lib/capybara-lockstep/middleware.rb +22 -0
- data/lib/capybara-lockstep/page_access.rb +28 -0
- data/lib/capybara-lockstep/server.rb +46 -0
- data/lib/capybara-lockstep/util.rb +21 -0
- data/lib/capybara-lockstep/version.rb +1 -1
- data/lib/capybara-lockstep.rb +5 -0
- metadata +13 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9d5a9ec157f65f4efb9be5ac2b33c5447552eb70e8d118438a7aad763c57492
|
4
|
+
data.tar.gz: 2ddde2a3b51079603d8368f0ebbe81d5205d473beeaf301bb6bdd42c80c968d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7d8723fe020460efad2e46555be0cf50d38e7398103e7462b8d0c21ac8260ee312bd8f7987e1a04ca471d6acb167b6b1e21565ef5ba73b19d021711bbb8743a
|
7
|
+
data.tar.gz: e364cdf67046d885d2f3b27404e258a37388cba10edb1f1f83f286fbc7023ff73f872558dfb33862e95ff6df2f3516bcd687a78094b27a4468ec86cc55a6032a
|
data/.github/workflows/test.yml
CHANGED
@@ -9,7 +9,7 @@ on:
|
|
9
9
|
- master
|
10
10
|
workflow_dispatch:
|
11
11
|
branches:
|
12
|
-
- master
|
12
|
+
- master
|
13
13
|
jobs:
|
14
14
|
test:
|
15
15
|
runs-on: ubuntu-20.04
|
@@ -18,8 +18,6 @@ jobs:
|
|
18
18
|
fail-fast: false
|
19
19
|
matrix:
|
20
20
|
include:
|
21
|
-
- ruby: 2.6.6
|
22
|
-
gemfile: Gemfile
|
23
21
|
- ruby: 2.7.2
|
24
22
|
gemfile: Gemfile
|
25
23
|
- ruby: 3.2.0
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file.
|
|
2
2
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
4
4
|
|
5
|
+
|
6
|
+
# 2.0.0
|
7
|
+
|
8
|
+
This major release detects many additional sources of flaky tests:
|
9
|
+
|
10
|
+
- We now synchronize before a user interaction. Previously we only synchronized before an observation. This could lead to race conditions when a test chained multiple interactions without [making an observation in between](https://makandracards.com/makandra/47336-fixing-flaky-e2e-tests#section-interleave-actions-and-expectations).
|
11
|
+
- We now synchronize after a user interaction (e.g. after a click). Previously we only synchronized before an observation. This could lead to race conditions when a test made assertions without going through Capybara, e.g. by accessing the database or global state variables.
|
12
|
+
- When a job ends (e.g. an AJAX request finishes) we now wait for one [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). This gives event listeners more time to schedule new async work.
|
13
|
+
- We now wait one JavaScript task after `touchstart`, `mousedown`, `click` and `keydown` events. This gives event listeners more time to schedule async work after a user interaction.
|
14
|
+
- You can now [wait while the backend server is busy](https://github.com/makandra/capybara-lockstep/#including-the-middleware-optional), by using `Capybara::Lockstep::Middleware` in your Rails or Rack app. We previously only waited for AJAX requests on the client, but using the middleware addresses some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
|
15
|
+
- You can [signal async work from the backend](https://github.com/makandra/capybara-lockstep/#on-the-backend), e.g. for background jobs. Note that you don't need to signal work for the regular request/response cycle, as this is detected automatically.
|
16
|
+
|
17
|
+
Although we now cover a lot more edge cases, this releases will not slow down your test suite considerably.
|
18
|
+
|
19
|
+
|
5
20
|
## 1.3.1 - 2023-10-25
|
6
21
|
|
7
22
|
Now synchronizes before and after `evaluate_script`.
|
data/Gemfile
CHANGED
@@ -5,17 +5,15 @@ source "https://rubygems.org"
|
|
5
5
|
# Specify your gem's dependencies in capybara-lockstep.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
+
gem 'activesupport', '~> 6.0'
|
8
9
|
gem "rake", "~> 13.0"
|
9
10
|
|
10
11
|
gem "rspec", "~> 3.0"
|
11
|
-
gem
|
12
|
+
gem "rspec-wait"
|
13
|
+
gem 'sinatra'
|
12
14
|
gem 'thin' # ruby 3 does not include a webserver
|
13
|
-
gem '
|
14
|
-
|
15
|
+
gem 'puma'
|
15
16
|
gem 'byebug'
|
16
17
|
gem 'gemika', '>= 0.8.1'
|
17
|
-
|
18
|
-
gem '
|
19
|
-
|
20
|
-
gem 'capybara', '= 3.36.0' # last version compatible with Ruby < 2.7, which is in our test matrix
|
21
|
-
gem 'selenium-webdriver', '=4.1.0' # last version compatible with Ruby < 2.7, which is in our test matrix
|
18
|
+
gem 'capybara', '>= 3'
|
19
|
+
gem 'selenium-webdriver', '>= 4'
|
data/Gemfile.lock
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
capybara-lockstep (
|
5
|
-
activesupport (>=
|
6
|
-
capybara (>=
|
4
|
+
capybara-lockstep (2.0.0)
|
5
|
+
activesupport (>= 4.2)
|
6
|
+
capybara (>= 3.0)
|
7
7
|
ruby2_keywords
|
8
|
-
selenium-webdriver (>=
|
8
|
+
selenium-webdriver (>= 4.0)
|
9
9
|
|
10
10
|
GEM
|
11
11
|
remote: https://rubygems.org/
|
@@ -16,10 +16,10 @@ GEM
|
|
16
16
|
minitest (>= 5.1)
|
17
17
|
tzinfo (~> 2.0)
|
18
18
|
zeitwerk (~> 2.3)
|
19
|
-
addressable (2.8.
|
19
|
+
addressable (2.8.5)
|
20
20
|
public_suffix (>= 2.0.2, < 6.0)
|
21
21
|
byebug (11.1.3)
|
22
|
-
capybara (3.
|
22
|
+
capybara (3.39.2)
|
23
23
|
addressable
|
24
24
|
matrix
|
25
25
|
mini_mime (>= 0.1.3)
|
@@ -29,36 +29,34 @@ GEM
|
|
29
29
|
regexp_parser (>= 1.5, < 3.0)
|
30
30
|
xpath (~> 3.2)
|
31
31
|
childprocess (4.1.0)
|
32
|
-
chrome_remote (0.3.0)
|
33
|
-
websocket-driver (~> 0.6)
|
34
32
|
concurrent-ruby (1.1.10)
|
35
|
-
daemons (1.
|
33
|
+
daemons (1.4.1)
|
36
34
|
diff-lcs (1.3)
|
37
35
|
eventmachine (1.2.7)
|
38
36
|
gemika (0.8.1)
|
39
37
|
i18n (1.12.0)
|
40
38
|
concurrent-ruby (~> 1.0)
|
41
|
-
jasmine (3.6.0)
|
42
|
-
jasmine-core (~> 3.6.0)
|
43
|
-
phantomjs
|
44
|
-
rack (>= 1.2.1)
|
45
|
-
rake
|
46
|
-
jasmine-core (3.6.0)
|
47
39
|
matrix (0.4.2)
|
48
|
-
mini_mime (1.1.
|
49
|
-
mini_portile2 (2.8.
|
40
|
+
mini_mime (1.1.5)
|
41
|
+
mini_portile2 (2.8.5)
|
50
42
|
minitest (5.16.3)
|
51
|
-
|
52
|
-
|
43
|
+
mustermann (3.0.0)
|
44
|
+
ruby2_keywords (~> 0.0.1)
|
45
|
+
nio4r (2.6.1)
|
46
|
+
nokogiri (1.15.5)
|
47
|
+
mini_portile2 (~> 2.8.2)
|
53
48
|
racc (~> 1.4)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
rack
|
49
|
+
public_suffix (5.0.4)
|
50
|
+
puma (6.4.0)
|
51
|
+
nio4r (~> 2.0)
|
52
|
+
racc (1.7.3)
|
53
|
+
rack (2.2.8)
|
54
|
+
rack-protection (3.1.0)
|
55
|
+
rack (~> 2.2, >= 2.2.4)
|
56
|
+
rack-test (2.1.0)
|
59
57
|
rack (>= 1.3)
|
60
|
-
rake (13.0
|
61
|
-
regexp_parser (2.
|
58
|
+
rake (13.1.0)
|
59
|
+
regexp_parser (2.8.2)
|
62
60
|
rexml (3.2.5)
|
63
61
|
rspec (3.7.0)
|
64
62
|
rspec-core (~> 3.7.0)
|
@@ -73,21 +71,26 @@ GEM
|
|
73
71
|
diff-lcs (>= 1.2.0, < 2.0)
|
74
72
|
rspec-support (~> 3.7.0)
|
75
73
|
rspec-support (3.7.0)
|
74
|
+
rspec-wait (0.0.9)
|
75
|
+
rspec (>= 3, < 4)
|
76
76
|
ruby2_keywords (0.0.5)
|
77
77
|
rubyzip (2.3.2)
|
78
78
|
selenium-webdriver (4.1.0)
|
79
79
|
childprocess (>= 0.5, < 5.0)
|
80
80
|
rexml (~> 3.2, >= 3.2.5)
|
81
81
|
rubyzip (>= 1.2.2)
|
82
|
-
|
82
|
+
sinatra (3.1.0)
|
83
|
+
mustermann (~> 3.0)
|
84
|
+
rack (~> 2.2, >= 2.2.4)
|
85
|
+
rack-protection (= 3.1.0)
|
86
|
+
tilt (~> 2.0)
|
87
|
+
thin (1.8.2)
|
83
88
|
daemons (~> 1.0, >= 1.0.9)
|
84
89
|
eventmachine (~> 1.0, >= 1.0.4)
|
85
90
|
rack (>= 1, < 3)
|
91
|
+
tilt (2.3.0)
|
86
92
|
tzinfo (2.0.5)
|
87
93
|
concurrent-ruby (~> 1.0)
|
88
|
-
websocket-driver (0.7.3)
|
89
|
-
websocket-extensions (>= 0.1.0)
|
90
|
-
websocket-extensions (0.1.5)
|
91
94
|
xpath (3.2.0)
|
92
95
|
nokogiri (~> 1.8)
|
93
96
|
zeitwerk (2.6.0)
|
@@ -98,14 +101,15 @@ PLATFORMS
|
|
98
101
|
DEPENDENCIES
|
99
102
|
activesupport (~> 6.0)
|
100
103
|
byebug
|
101
|
-
capybara (
|
104
|
+
capybara (>= 3)
|
102
105
|
capybara-lockstep!
|
103
|
-
chrome_remote
|
104
106
|
gemika (>= 0.8.1)
|
105
|
-
|
107
|
+
puma
|
106
108
|
rake (~> 13.0)
|
107
109
|
rspec (~> 3.0)
|
108
|
-
|
110
|
+
rspec-wait
|
111
|
+
selenium-webdriver (>= 4)
|
112
|
+
sinatra
|
109
113
|
thin
|
110
114
|
|
111
115
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
This Ruby gem synchronizes [Capybara](https://github.com/teamcapybara/capybara) commands with client-side JavaScript and AJAX requests. This greatly improves the stability of an end-to-end ("E2E") test suite, even if that suite has timing issues.
|
4
4
|
|
5
5
|
The next section explain why your test suite is flaky and how capybara-lockstep can help.\
|
6
|
-
If you don't care you may skip to [installation instructions](#installation)
|
6
|
+
If you don't care you may **skip to [installation instructions](#installation)**.
|
7
7
|
|
8
8
|
|
9
9
|
Why are tests flaky?
|
@@ -58,8 +58,9 @@ How capybara-lockstep helps
|
|
58
58
|
|
59
59
|
capybara-lockstep waits until the browser is idle before moving on to the next Capybara command. This greatly relieves the pressure on [Capybara's retry logic](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends).
|
60
60
|
|
61
|
-
capybara-lockstep synchronizes
|
61
|
+
capybara-lockstep synchronizes when one of the following occurs:
|
62
62
|
|
63
|
+
- Capybara looks up an element
|
63
64
|
- Capybara simulates a user interaction (clicking, typing, etc.)
|
64
65
|
- Capybara visits a new URL
|
65
66
|
- Capybara executes JavaScript
|
@@ -69,13 +70,24 @@ When capybara-lockstep synchronizes it will:
|
|
69
70
|
- wait for all document resources to load (images, CSS, fonts, frames).
|
70
71
|
- wait for client-side JavaScript to render or hydrate DOM elements.
|
71
72
|
- wait for any pending AJAX requests to finish and their callbacks to be called.
|
72
|
-
-
|
73
|
-
-
|
73
|
+
- wait for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
|
74
|
+
- waits for dynamically inserted `<img>` or `<iframe>` elements to load.
|
74
75
|
|
75
|
-
In summary Capybara can no longer observe the page while HTTP requests are in flight.
|
76
|
+
In summary Capybara can no longer observe or interact with the page while HTTP requests are in flight.
|
76
77
|
This covers most async work that causes flaky tests.
|
77
78
|
|
78
|
-
|
79
|
+
|
80
|
+
### Limitations
|
81
|
+
|
82
|
+
Async work not synchronized by capybara-lockstep includes:
|
83
|
+
|
84
|
+
- Animations
|
85
|
+
- Websocket connections
|
86
|
+
- Media elements (`<video>`, `<audio>`)
|
87
|
+
- Service workers
|
88
|
+
- Work scheduled via `setTimeout()` or `setInterval()`.
|
89
|
+
|
90
|
+
You can configure capybara-lockstep to [wait for additional async work](#signaling-asynchronous-work).
|
79
91
|
|
80
92
|
|
81
93
|
Installation
|
@@ -85,8 +97,8 @@ Installation
|
|
85
97
|
|
86
98
|
Check if your application satisfies all requirements for capybara-lockstep:
|
87
99
|
|
88
|
-
- Capybara 2 or higher.
|
89
|
-
- Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-lockstep deactivates itself for any other driver.
|
100
|
+
- Capybara 2.0 or higher.
|
101
|
+
- Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/) 3.0 or higher. capybara-lockstep deactivates itself for any other driver.
|
90
102
|
- This gem was only tested with a Selenium-controlled Chrome browser. [Chrome in headless mode](https://makandracards.com/makandra/492109-running-capybara-tests-in-headless-chrome) is recommended, but not required.
|
91
103
|
- This gem was only tested with Rails, but there's no Rails dependency.
|
92
104
|
|
@@ -110,7 +122,7 @@ $ bundle install
|
|
110
122
|
If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec), `test_helper.rb` (Minitest) or `env.rb` (Cucumber).
|
111
123
|
|
112
124
|
|
113
|
-
### Including the JavaScript snippet
|
125
|
+
### Including the JavaScript snippet (required)
|
114
126
|
|
115
127
|
capybara-lockstep requires a JavaScript snippet to be embedded by the application under test. If that snippet is missing on a screen, capybara-lockstep will not be able to synchronize with the browser. In that case the test will continue without synchronization.
|
116
128
|
|
@@ -122,11 +134,31 @@ capybara-lockstep requires a JavaScript snippet to be embedded by the applicatio
|
|
122
134
|
|
123
135
|
Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
|
124
136
|
|
125
|
-
**If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript with `
|
137
|
+
**If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript code with `capybara_lockstep_js`.
|
126
138
|
|
127
139
|
**If you have a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)** the `capybara_lockstep` Rails helper will insert a CSP nonce by default. You can also pass an explicit nonce string using the `:nonce` option.
|
128
140
|
|
129
141
|
|
142
|
+
### Including the middleware (optional)
|
143
|
+
|
144
|
+
This gem provides a Rack middleware to block Capybara while your Rails (or Rack) backend is busy.
|
145
|
+
|
146
|
+
Using the middleware is optional, as the [JavaScript snippet](#including-the-javascript-snippet-required) already for AJAX requests on the client. However, using the middleware covers some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
|
147
|
+
|
148
|
+
To include the middleware in a Rails application, add the following line to `config/environments/test.rb`:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
config.middleware.insert_before 0, Capybara::Lockstep::Middleware
|
152
|
+
```
|
153
|
+
|
154
|
+
In a **non-Rails** application you should include the middleware as high up in your middleware stack as possible:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
use Capybara::Lockstep::Middleware
|
158
|
+
# Other middleware here
|
159
|
+
```
|
160
|
+
|
161
|
+
|
130
162
|
|
131
163
|
### Verify successful integration
|
132
164
|
|
@@ -152,16 +184,22 @@ Note that you may see some failures from tests with wrong assertions, which prev
|
|
152
184
|
|
153
185
|
## Signaling asynchronous work
|
154
186
|
|
155
|
-
By default capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
|
187
|
+
[By default](#how-capybara-lockstep-helps) capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
|
188
|
+
There are also some [limitations](#limitations).
|
189
|
+
|
190
|
+
You can configure capybara-lockstep to wait for other async work.
|
156
191
|
|
157
|
-
|
192
|
+
|
193
|
+
### On the frontend
|
194
|
+
|
195
|
+
Let's say we have an animation that fades in a new element over 2 seconds. The following will block Capybara while the animation is running:
|
158
196
|
|
159
197
|
```js
|
160
198
|
async function fadeIn(element) {
|
161
|
-
CapybaraLockstep
|
199
|
+
CapybaraLockstep?.startWork('Animation')
|
162
200
|
startAnimation(element, 'fade-in')
|
163
201
|
await waitForAnimationEnd(element)
|
164
|
-
CapybaraLockstep
|
202
|
+
CapybaraLockstep?.stopWork('Animation')
|
165
203
|
}
|
166
204
|
```
|
167
205
|
|
@@ -175,20 +213,24 @@ The string argument is used for logging (when logging is enabled). It does **not
|
|
175
213
|
You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
|
176
214
|
|
177
215
|
|
178
|
-
|
216
|
+
### On the backend
|
179
217
|
|
180
|
-
|
218
|
+
You don't need to signal work within the regular request/response cycle, as this is detected automatically. You can however signal
|
219
|
+
work that happens outside a request, e.g. in a background job or WebSocket handler.
|
181
220
|
|
182
|
-
|
183
|
-
if (window.CapybaraLockstep) {
|
184
|
-
// interact with CapybaraLockstep
|
185
|
-
}
|
186
|
-
```
|
187
|
-
|
188
|
-
If you can use ES6 you may also use [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) to only call a function if `window.CapybaraLockstep` is defined:
|
221
|
+
The following will block Capybara while a [Sidekiq](https://sidekiq.org/) job is running:
|
189
222
|
|
190
|
-
```
|
191
|
-
|
223
|
+
```ruby
|
224
|
+
class HardJob
|
225
|
+
include Sidekiq::Job
|
226
|
+
|
227
|
+
def perform(name, count)
|
228
|
+
Capybara::Lockstep.start_work('HardJob') if defined?(Capybara::Lockstep)
|
229
|
+
# do something
|
230
|
+
ensure
|
231
|
+
Capybara::Lockstep.stop_work('StopWork') if defined?(Capybara::Lockstep)
|
232
|
+
end
|
233
|
+
end
|
192
234
|
```
|
193
235
|
|
194
236
|
|
@@ -335,14 +377,13 @@ It is theoretically possible that your test will observe the browser in that win
|
|
335
377
|
Any issues caused by this will usually be mitigated by Capybara's retry logic. **If** you think that this is an issue for your test suite, you can configure capybara-headless to wait additional tasks before it considers the browser to be idle:
|
336
378
|
|
337
379
|
```ruby
|
338
|
-
Capybara::Lockstep.wait_tasks = 1
|
380
|
+
Capybara::Lockstep.wait_tasks = 2 // default is 1
|
339
381
|
```
|
340
382
|
|
341
383
|
If you see longer chains of `then()` or nested `setTimeout()` calls in your code, you may need to configure a higher number of tasks to wait.
|
342
384
|
|
343
385
|
Waiting additional tasks will have a negative performance impact on your test suite.
|
344
386
|
|
345
|
-
> **Note:** When capybara-lockstep detects jQuery on the page, it will automatically patch [`$.ajax()`](https://api.jquery.com/jQuery.ajax/) to wait an additional task after the response was received. If your only concern is callbacks to `$.ajax()` you do not need so set `Capybara::Lockstep.wait_tasks`.
|
346
387
|
|
347
388
|
|
348
389
|
## Running code after synchronization
|
data/Rakefile
CHANGED
data/capybara-lockstep.gemspec
CHANGED
@@ -28,9 +28,9 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
30
|
# Uncomment to register a new dependency of your gem
|
31
|
-
spec.add_dependency "capybara", ">=
|
32
|
-
spec.add_dependency "selenium-webdriver", ">=
|
33
|
-
spec.add_dependency "activesupport", ">=
|
31
|
+
spec.add_dependency "capybara", ">= 3.0"
|
32
|
+
spec.add_dependency "selenium-webdriver", ">= 4.0"
|
33
|
+
spec.add_dependency "activesupport", ">= 4.2"
|
34
34
|
spec.add_dependency "ruby2_keywords"
|
35
35
|
|
36
36
|
# For more information and examples about making a new gem, checkout our
|
@@ -2,13 +2,27 @@ require 'ruby2_keywords'
|
|
2
2
|
|
3
3
|
module Capybara
|
4
4
|
module Lockstep
|
5
|
-
module
|
6
|
-
|
5
|
+
module SynchronizeMacros
|
6
|
+
|
7
|
+
def synchronize_before(meth, lazy:)
|
8
|
+
mod = Module.new do
|
9
|
+
define_method meth do |*args, &block|
|
10
|
+
Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
|
11
|
+
super(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
ruby2_keywords meth
|
15
|
+
end
|
16
|
+
|
17
|
+
prepend(mod)
|
18
|
+
end
|
19
|
+
|
20
|
+
def synchronize_after(meth)
|
7
21
|
mod = Module.new do
|
8
22
|
define_method meth do |*args, &block|
|
9
23
|
super(*args, &block)
|
10
24
|
ensure
|
11
|
-
Lockstep.
|
25
|
+
Lockstep.auto_synchronize
|
12
26
|
end
|
13
27
|
|
14
28
|
ruby2_keywords meth
|
@@ -16,18 +30,13 @@ module Capybara
|
|
16
30
|
|
17
31
|
prepend(mod)
|
18
32
|
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
33
|
|
23
|
-
|
24
|
-
module Lockstep
|
25
|
-
module SynchronizeBefore
|
26
|
-
def synchronize_before(meth, lazy:)
|
34
|
+
def unsynchronize_after(meth)
|
27
35
|
mod = Module.new do
|
28
36
|
define_method meth do |*args, &block|
|
29
|
-
Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
|
30
37
|
super(*args, &block)
|
38
|
+
ensure
|
39
|
+
Lockstep.unsynchronize
|
31
40
|
end
|
32
41
|
|
33
42
|
ruby2_keywords meth
|
@@ -35,32 +44,32 @@ module Capybara
|
|
35
44
|
|
36
45
|
prepend(mod)
|
37
46
|
end
|
47
|
+
|
38
48
|
end
|
39
49
|
end
|
40
50
|
end
|
41
51
|
|
42
52
|
Capybara::Session.class_eval do
|
43
|
-
extend Capybara::Lockstep::
|
44
|
-
extend Capybara::Lockstep::UnsychronizeAfter
|
53
|
+
extend Capybara::Lockstep::SynchronizeMacros
|
45
54
|
|
46
55
|
synchronize_before :html, lazy: true # wait until running JavaScript has updated the DOM
|
47
56
|
|
48
57
|
synchronize_before :current_url, lazy: true # wait until running JavaScript has updated the URL
|
49
58
|
|
50
59
|
synchronize_before :refresh, lazy: false # wait until running JavaScript has updated the URL
|
51
|
-
|
60
|
+
unsynchronize_after :refresh # new document is no longer synchronized
|
52
61
|
|
53
62
|
synchronize_before :go_back, lazy: false # wait until running JavaScript has updated the URL
|
54
|
-
|
63
|
+
unsynchronize_after :go_back # new document is no longer synchronized
|
55
64
|
|
56
65
|
synchronize_before :go_forward, lazy: false # wait until running JavaScript has updated the URL
|
57
|
-
|
66
|
+
unsynchronize_after :go_forward # new document is no longer synchronized
|
58
67
|
|
59
68
|
synchronize_before :switch_to_frame, lazy: true # wait until the current frame is done processing
|
60
|
-
|
69
|
+
unsynchronize_after :switch_to_frame # now that we've switched into the new frame, we don't know the document's synchronization state.
|
61
70
|
|
62
71
|
synchronize_before :switch_to_window, lazy: true # wait until the current frame is done processing
|
63
|
-
|
72
|
+
unsynchronize_after :switch_to_window # now that we've switched to the new window, we don't know the document's synchronization state.
|
64
73
|
end
|
65
74
|
|
66
75
|
module Capybara
|
@@ -88,7 +97,7 @@ module Capybara
|
|
88
97
|
super(*args, &block).tap do
|
89
98
|
if visiting_real_url
|
90
99
|
# We haven't yet synchronized the new screen.
|
91
|
-
Lockstep.
|
100
|
+
Lockstep.unsynchronize
|
92
101
|
end
|
93
102
|
end
|
94
103
|
end
|
@@ -128,7 +137,7 @@ module Capybara
|
|
128
137
|
if !Lockstep.synchronizing?
|
129
138
|
# We haven't yet synchronized with whatever changes the JavaScript
|
130
139
|
# did on the frontend.
|
131
|
-
Lockstep.
|
140
|
+
Lockstep.unsynchronize
|
132
141
|
end
|
133
142
|
end
|
134
143
|
|
@@ -167,21 +176,59 @@ end
|
|
167
176
|
|
168
177
|
node_classes.each do |node_class|
|
169
178
|
node_class.class_eval do
|
170
|
-
extend Capybara::Lockstep::
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
179
|
+
extend Capybara::Lockstep::SynchronizeMacros
|
180
|
+
|
181
|
+
synchronize_before :set, lazy: true
|
182
|
+
unsynchronize_after :set
|
183
|
+
synchronize_after :set
|
184
|
+
|
185
|
+
synchronize_before :select_option, lazy: true
|
186
|
+
unsynchronize_after :select_option
|
187
|
+
synchronize_after :select_option
|
188
|
+
|
189
|
+
synchronize_before :unselect_option, lazy: true
|
190
|
+
unsynchronize_after :unselect_option
|
191
|
+
synchronize_after :unselect_option
|
192
|
+
|
193
|
+
synchronize_before :click, lazy: true
|
194
|
+
unsynchronize_after :click
|
195
|
+
synchronize_after :click
|
196
|
+
|
197
|
+
synchronize_before :right_click, lazy: true
|
198
|
+
unsynchronize_after :right_click
|
199
|
+
synchronize_after :right_click
|
200
|
+
|
201
|
+
synchronize_before :double_click, lazy: true
|
202
|
+
unsynchronize_after :double_click
|
203
|
+
synchronize_after :double_click
|
204
|
+
|
205
|
+
synchronize_before :send_keys, lazy: true
|
206
|
+
unsynchronize_after :send_keys
|
207
|
+
synchronize_after :send_keys
|
208
|
+
|
209
|
+
synchronize_before :hover, lazy: true
|
210
|
+
unsynchronize_after :hover
|
211
|
+
synchronize_after :hover
|
212
|
+
|
213
|
+
synchronize_before :drag_to, lazy: true
|
214
|
+
unsynchronize_after :drag_to
|
215
|
+
synchronize_after :drag_to
|
216
|
+
|
217
|
+
synchronize_before :drop, lazy: true
|
218
|
+
unsynchronize_after :drop
|
219
|
+
synchronize_after :drop
|
220
|
+
|
221
|
+
synchronize_before :scroll_by, lazy: true
|
222
|
+
unsynchronize_after :scroll_by
|
223
|
+
synchronize_after :scroll_by
|
224
|
+
|
225
|
+
synchronize_before :scroll_to, lazy: true
|
226
|
+
unsynchronize_after :scroll_to
|
227
|
+
synchronize_after :scroll_to
|
228
|
+
|
229
|
+
synchronize_before :trigger, lazy: true
|
230
|
+
unsynchronize_after :trigger
|
231
|
+
synchronize_after :trigger
|
185
232
|
end
|
186
233
|
end
|
187
234
|
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
class Client
|
4
|
+
include Logging
|
5
|
+
include PageAccess
|
6
|
+
|
7
|
+
ERROR_SNIPPET_MISSING = 'Cannot synchronize: capybara-lockstep JavaScript snippet is missing'
|
8
|
+
ERROR_PAGE_MISSING = 'Cannot synchronize with empty page'
|
9
|
+
ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
|
10
|
+
ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
|
11
|
+
|
12
|
+
SYNCHRONIZED_IVAR = :@lockstep_synchronized_client
|
13
|
+
|
14
|
+
def synchronized?
|
15
|
+
# The synchronized flag is per-session (page == Capybara.current_session).
|
16
|
+
# This enables tests that use more than one browser, e.g. to test multi-user interaction:
|
17
|
+
# https://makandracards.com/makandra/474480-how-to-make-a-cucumber-test-work-with-multiple-browser-sessions
|
18
|
+
#
|
19
|
+
# Ideally the synchronized flag would also be per-tab, per-frame and per-document.
|
20
|
+
# We haven't found a way to patch this into Capybara, as there does not seem to be
|
21
|
+
# a persistent object representing a document. Capybara::Node::Document just seems to
|
22
|
+
# be a proxy accessing whatever is the current document. The way we work around this
|
23
|
+
# is that we synchronize before switching tabs or frames.
|
24
|
+
value = page.instance_variable_get(SYNCHRONIZED_IVAR)
|
25
|
+
|
26
|
+
# We consider a new Capybara session to be synchronized.
|
27
|
+
# This will be set to false after our first visit().
|
28
|
+
value.nil? ? true : value
|
29
|
+
end
|
30
|
+
|
31
|
+
def synchronized=(value)
|
32
|
+
page.instance_variable_set(SYNCHRONIZED_IVAR, value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def synchronize
|
36
|
+
# If synchronization fails below we consider us unsynchronized after.
|
37
|
+
self.synchronized = false
|
38
|
+
|
39
|
+
# Running the synchronization script while an alert is open would close the alert,
|
40
|
+
# most likely causing subsequent expectations to fail.
|
41
|
+
if alert_present?
|
42
|
+
log ERROR_ALERT_OPEN
|
43
|
+
# Don't raise an error, this will happen in an innocent test.
|
44
|
+
# We will retry on the next Capybara synchronize call.
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
start_time = Util.current_seconds
|
49
|
+
|
50
|
+
begin
|
51
|
+
Util.with_max_wait_time(timeout) do
|
52
|
+
message_from_js = evaluate_async_script(<<~JS)
|
53
|
+
let done = arguments[0]
|
54
|
+
let synchronize = () => {
|
55
|
+
if (window.CapybaraLockstep) {
|
56
|
+
CapybaraLockstep.synchronize(done)
|
57
|
+
} else {
|
58
|
+
done(#{ERROR_SNIPPET_MISSING.to_json})
|
59
|
+
}
|
60
|
+
}
|
61
|
+
const emptyDataURL = /^data:[^,]*,?$/
|
62
|
+
if (emptyDataURL.test(location.href) || location.protocol === 'about:') {
|
63
|
+
done(#{ERROR_PAGE_MISSING.to_json})
|
64
|
+
} else if (document.readyState === 'complete') {
|
65
|
+
// WebDriver always waits for the `load` event after a visit(),
|
66
|
+
// unless a different page load strategy was configured.
|
67
|
+
synchronize()
|
68
|
+
} else {
|
69
|
+
window.addEventListener('load', synchronize)
|
70
|
+
}
|
71
|
+
JS
|
72
|
+
|
73
|
+
case message_from_js
|
74
|
+
when ERROR_PAGE_MISSING
|
75
|
+
log(message_from_js)
|
76
|
+
when ERROR_SNIPPET_MISSING
|
77
|
+
log(message_from_js)
|
78
|
+
else
|
79
|
+
log message_from_js
|
80
|
+
end_time = Util.current_seconds
|
81
|
+
ms_elapsed = ((end_time.to_f - start_time) * 1000).round
|
82
|
+
log "Synchronized client successfully [#{ms_elapsed} ms]"
|
83
|
+
self.synchronized = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
|
87
|
+
timeout_message = "Could not synchronize client within #{timeout} seconds"
|
88
|
+
log timeout_message
|
89
|
+
if timeout_with == :error
|
90
|
+
raise Timeout, timeout_message
|
91
|
+
else
|
92
|
+
# Don't raise an error, this may happen if the server is slow to respond.
|
93
|
+
# We will retry on the next Capybara synchronize call.
|
94
|
+
end
|
95
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
96
|
+
log ERROR_ALERT_OPEN
|
97
|
+
# Don't raise an error, this will happen in an innocent test.
|
98
|
+
# We will retry on the next Capybara synchronize call.
|
99
|
+
rescue ::Selenium::WebDriver::Error::JavascriptError => e
|
100
|
+
# When the URL changes while a script is running, my current selenium-webdriver
|
101
|
+
# raises a Selenium::WebDriver::Error::JavascriptError with the message:
|
102
|
+
# "javascript error: document unloaded while waiting for result".
|
103
|
+
# We will retry on the next Capybara synchronize call, by then we should see
|
104
|
+
# the new page.
|
105
|
+
if e.message.include?('unload')
|
106
|
+
log ERROR_NAVIGATED_AWAY
|
107
|
+
else
|
108
|
+
unhandled_synchronize_error(e)
|
109
|
+
end
|
110
|
+
rescue StandardError => e
|
111
|
+
unhandled_synchronize_error(e)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def unhandled_synchronize_error(e)
|
118
|
+
Lockstep.log "#{e.class.name} while synchronizing: #{e.message}"
|
119
|
+
raise e
|
120
|
+
end
|
121
|
+
|
122
|
+
def timeout
|
123
|
+
Lockstep.timeout
|
124
|
+
end
|
125
|
+
|
126
|
+
def timeout_with
|
127
|
+
Lockstep.timeout_with
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
@@ -18,9 +18,13 @@ module Capybara
|
|
18
18
|
@timeout_with = action&.to_sym
|
19
19
|
end
|
20
20
|
|
21
|
+
def debug
|
22
|
+
@debug.nil? ? false : @debug
|
23
|
+
end
|
24
|
+
|
21
25
|
def debug?
|
22
26
|
# @debug may also be a Logger object, so convert it to a boolean
|
23
|
-
|
27
|
+
!!debug
|
24
28
|
end
|
25
29
|
|
26
30
|
def debug=(value)
|
@@ -49,6 +53,14 @@ module Capybara
|
|
49
53
|
@mode = mode&.to_sym
|
50
54
|
end
|
51
55
|
|
56
|
+
def with_mode(temporary_mode, &block)
|
57
|
+
old_mode = mode
|
58
|
+
self.mode = temporary_mode
|
59
|
+
block.call
|
60
|
+
ensure
|
61
|
+
self.mode = temporary_mode
|
62
|
+
end
|
63
|
+
|
52
64
|
def enabled=(enabled)
|
53
65
|
case enabled
|
54
66
|
when true
|
@@ -93,13 +105,11 @@ module Capybara
|
|
93
105
|
end
|
94
106
|
end
|
95
107
|
|
96
|
-
def javascript_driver?
|
97
|
-
driver.is_a?(Capybara::Selenium::Driver)
|
98
|
-
end
|
99
|
-
|
100
108
|
def send_config_to_browser(js)
|
109
|
+
return unless javascript_driver?
|
110
|
+
|
101
111
|
begin
|
102
|
-
with_max_wait_time(2) do
|
112
|
+
Util.with_max_wait_time(2) do
|
103
113
|
page.execute_script(<<~JS)
|
104
114
|
if (window.CapybaraLockstep) {
|
105
115
|
#{js}
|
@@ -11,7 +11,7 @@ window.CapybaraLockstep = (function() {
|
|
11
11
|
jobCount = 0
|
12
12
|
idleCallbacks = []
|
13
13
|
finishedWorkTags = []
|
14
|
-
waitTasks =
|
14
|
+
waitTasks = 1
|
15
15
|
debug = false
|
16
16
|
}
|
17
17
|
|
@@ -58,18 +58,7 @@ window.CapybaraLockstep = (function() {
|
|
58
58
|
}
|
59
59
|
|
60
60
|
function stopWork(tag) {
|
61
|
-
|
62
|
-
|
63
|
-
let check = function() {
|
64
|
-
if (tasksElapsed < waitTasks) {
|
65
|
-
tasksElapsed++
|
66
|
-
setTimeout(check)
|
67
|
-
} else {
|
68
|
-
stopWorkNow(tag)
|
69
|
-
}
|
70
|
-
}
|
71
|
-
|
72
|
-
check()
|
61
|
+
afterWaitTasks(stopWorkNow.bind(this, tag))
|
73
62
|
}
|
74
63
|
|
75
64
|
function stopWorkNow(tag) {
|
@@ -266,6 +255,14 @@ window.CapybaraLockstep = (function() {
|
|
266
255
|
}
|
267
256
|
}
|
268
257
|
|
258
|
+
function afterWaitTasks(fn, tasksLeft = waitTasks) {
|
259
|
+
if (tasksLeft > 0) {
|
260
|
+
afterWaitTasks(fn, tasksLeft - 1)
|
261
|
+
} else {
|
262
|
+
fn()
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
269
266
|
function trackOldUnpoly() {
|
270
267
|
// CapybaraLockstep.track() is called as the first script in the head.
|
271
268
|
// Unpoly will be loaded after us, so we wait until DOMContentReady.
|
@@ -282,12 +279,32 @@ window.CapybaraLockstep = (function() {
|
|
282
279
|
})
|
283
280
|
}
|
284
281
|
|
282
|
+
function trackInteraction(eventType) {
|
283
|
+
document.addEventListener(eventType, function() {
|
284
|
+
// Only litter the log with interaction events if we're actually going
|
285
|
+
// to be busy for at least 1 task.
|
286
|
+
if (waitTasks > 0) {
|
287
|
+
let tag = eventType
|
288
|
+
startWork(tag)
|
289
|
+
stopWork(tag)
|
290
|
+
}
|
291
|
+
})
|
292
|
+
}
|
293
|
+
|
285
294
|
function track() {
|
286
295
|
trackOldUnpoly()
|
287
296
|
trackFetch()
|
288
297
|
trackXHR()
|
289
298
|
trackRemoteElements()
|
290
299
|
trackJQuery()
|
300
|
+
trackInteraction('touchstart')
|
301
|
+
trackInteraction('mousedown')
|
302
|
+
trackInteraction('click')
|
303
|
+
trackInteraction('keydown')
|
304
|
+
trackInteraction('focusin')
|
305
|
+
trackInteraction('focusout')
|
306
|
+
trackInteraction('input')
|
307
|
+
trackInteraction('change')
|
291
308
|
}
|
292
309
|
|
293
310
|
function synchronize(callback) {
|
@@ -1,39 +1,31 @@
|
|
1
1
|
module Capybara
|
2
2
|
module Lockstep
|
3
|
-
ERROR_SNIPPET_MISSING = 'Cannot synchronize: capybara-lockstep JavaScript snippet is missing'
|
4
|
-
ERROR_PAGE_MISSING = 'Cannot synchronize with empty page'
|
5
|
-
ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
|
6
|
-
ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
|
7
|
-
|
8
3
|
class << self
|
9
4
|
include Configuration
|
10
5
|
include Logging
|
6
|
+
include PageAccess
|
11
7
|
|
12
8
|
attr_accessor :synchronizing
|
13
9
|
alias synchronizing? synchronizing
|
14
10
|
|
15
|
-
def
|
16
|
-
|
17
|
-
# This enables tests that use more than one browser, e.g. to test multi-user interaction:
|
18
|
-
# https://makandracards.com/makandra/474480-how-to-make-a-cucumber-test-work-with-multiple-browser-sessions
|
19
|
-
#
|
20
|
-
# Ideally the synchronized flag would also be per-tab, per-frame and per-document.
|
21
|
-
# We haven't found a way to patch this into Capybara, as there does not seem to be
|
22
|
-
# a persistent object representing a document. Capybara::Node::Document just seems to
|
23
|
-
# be a proxy accessing whatever is the current document. The way we work around this
|
24
|
-
# is that we synchronize before switching tabs or frames.
|
25
|
-
value = page.instance_variable_get(:@lockstep_synchronized)
|
26
|
-
|
27
|
-
# We consider a new Capybara session to be synchronized.
|
28
|
-
# This will be set to false after our first visit().
|
29
|
-
value.nil? ? true : value
|
11
|
+
def unsynchronize
|
12
|
+
client.synchronized = false
|
30
13
|
end
|
31
14
|
|
32
|
-
|
33
|
-
|
15
|
+
# Automatic synchronization from within the capybara-lockstep should always call #auto_synchronize.
|
16
|
+
# This only synchronizes IFF in :auto mode, i.e. the user has not explicitly disabled automatic syncing.
|
17
|
+
# The :auto mode has nothing to do with the { lazy } option.
|
18
|
+
def auto_synchronize(**options)
|
19
|
+
if mode == :auto
|
20
|
+
synchronize(**options)
|
21
|
+
end
|
34
22
|
end
|
35
23
|
|
36
|
-
def synchronize(lazy: false, log:
|
24
|
+
def synchronize(lazy: false, log: 'Synchronizing')
|
25
|
+
if synchronizing? || mode == :off
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
37
29
|
# The { lazy } option is a performance optimization that will prevent capybara-lockstep
|
38
30
|
# from synchronizing multiple times in expressions like `page.find('.foo').find('.bar')`.
|
39
31
|
# The { lazy } option has nothing todo with :auto mode.
|
@@ -48,135 +40,38 @@ module Capybara
|
|
48
40
|
# thinks we're in sync or not. This always makes an execute_script() rountrip, but picks up
|
49
41
|
# non-lazy synchronization so we pick up client-side work that have not been caused
|
50
42
|
# by Capybara commands.
|
51
|
-
|
52
|
-
return
|
53
|
-
end
|
54
|
-
|
55
|
-
synchronize_now(log: log)
|
56
|
-
|
57
|
-
run_after_synchronize_callbacks
|
58
|
-
end
|
59
|
-
|
60
|
-
# Automatic synchronization from within the capybara-lockstep should always call #auto_synchronize.
|
61
|
-
# This only synchronizes IFF in :auto mode, i.e. the user has not explicitly disabled automatic syncing.
|
62
|
-
# The :auto mode has nothing to do with the { lazy } option.
|
63
|
-
def auto_synchronize(**options)
|
64
|
-
if mode == :auto
|
65
|
-
synchronize(**options)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def synchronize_now(log: 'Synchronizing')
|
72
|
-
self.synchronizing = true
|
73
|
-
self.synchronized = false
|
74
|
-
|
75
|
-
self.log(log)
|
76
|
-
|
77
|
-
start_time = current_seconds
|
43
|
+
will_synchronize_client = !(lazy && client.synchronized?)
|
78
44
|
|
79
45
|
begin
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
}
|
90
|
-
const emptyDataURL = /^data:[^,]*,?$/
|
91
|
-
if (emptyDataURL.test(location.href) || location.protocol === 'about:') {
|
92
|
-
done(#{ERROR_PAGE_MISSING.to_json})
|
93
|
-
} else if (document.readyState === 'complete') {
|
94
|
-
// WebDriver always waits for the `load` event after a visit(),
|
95
|
-
// unless a different page load strategy was configured.
|
96
|
-
synchronize()
|
97
|
-
} else {
|
98
|
-
window.addEventListener('load', synchronize)
|
99
|
-
}
|
100
|
-
JS
|
101
|
-
|
102
|
-
case message_from_js
|
103
|
-
when ERROR_PAGE_MISSING
|
104
|
-
log(message_from_js)
|
105
|
-
when ERROR_SNIPPET_MISSING
|
106
|
-
log(message_from_js)
|
107
|
-
else
|
108
|
-
log message_from_js
|
109
|
-
end_time = current_seconds
|
110
|
-
ms_elapsed = ((end_time.to_f - start_time) * 1000).round
|
111
|
-
log "Synchronized successfully [#{ms_elapsed} ms]"
|
112
|
-
self.synchronized = true
|
113
|
-
end
|
114
|
-
end
|
115
|
-
rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
|
116
|
-
timeout_message = "Could not synchronize within #{timeout} seconds"
|
117
|
-
log timeout_message
|
118
|
-
if timeout_with == :error
|
119
|
-
raise Timeout, timeout_message
|
120
|
-
else
|
121
|
-
# Don't raise an error, this may happen if the server is slow to respond.
|
122
|
-
# We will retry on the next Capybara synchronize call.
|
46
|
+
# Synchronizing the server is free, so we ignore { lazy } and do it every time.
|
47
|
+
server.synchronize
|
48
|
+
|
49
|
+
if will_synchronize_client
|
50
|
+
self.log(log)
|
51
|
+
self.synchronizing = true
|
52
|
+
client.synchronize
|
53
|
+
# Synchronizing the server is free, so we ignore { lazy } and do it every time.
|
54
|
+
server.synchronize
|
123
55
|
end
|
124
|
-
|
125
|
-
|
126
|
-
# Don't raise an error, this will happen in an innocent test.
|
127
|
-
# We will retry on the next Capybara synchronize call.
|
128
|
-
rescue ::Selenium::WebDriver::Error::JavascriptError => e
|
129
|
-
# When the URL changes while a script is running, my current selenium-webdriver
|
130
|
-
# raises a Selenium::WebDriver::Error::JavascriptError with the message:
|
131
|
-
# "javascript error: document unloaded while waiting for result".
|
132
|
-
# We will retry on the next Capybara synchronize call, by then we should see
|
133
|
-
# the new page.
|
134
|
-
if e.message.include?('unload')
|
135
|
-
log ERROR_NAVIGATED_AWAY
|
136
|
-
else
|
137
|
-
unhandled_synchronize_error(e)
|
138
|
-
end
|
139
|
-
rescue StandardError => e
|
140
|
-
unhandled_synchronize_error(e)
|
56
|
+
ensure
|
57
|
+
self.synchronizing = false
|
141
58
|
end
|
142
59
|
|
143
|
-
|
144
|
-
|
145
|
-
end
|
146
|
-
|
147
|
-
def unhandled_synchronize_error(e)
|
148
|
-
log "#{e.class.name} while synchronizing: #{e.message}"
|
149
|
-
raise e
|
150
|
-
end
|
151
|
-
|
152
|
-
def page
|
153
|
-
Capybara.current_session
|
154
|
-
end
|
155
|
-
|
156
|
-
delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
|
157
|
-
|
158
|
-
def with_max_wait_time(seconds, &block)
|
159
|
-
old_max_wait_time = Capybara.default_max_wait_time
|
160
|
-
Capybara.default_max_wait_time = seconds
|
161
|
-
begin
|
162
|
-
block.call
|
163
|
-
ensure
|
164
|
-
Capybara.default_max_wait_time = old_max_wait_time
|
60
|
+
if will_synchronize_client
|
61
|
+
run_after_synchronize_callbacks
|
165
62
|
end
|
166
63
|
end
|
167
64
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
65
|
+
delegate :start_work, :stop_work, to: :server
|
66
|
+
|
67
|
+
def server
|
68
|
+
@server ||= Server.new
|
172
69
|
end
|
173
70
|
|
174
|
-
def
|
175
|
-
|
71
|
+
def client
|
72
|
+
@client ||= Client.new
|
176
73
|
end
|
177
74
|
|
178
75
|
end
|
179
|
-
|
180
76
|
end
|
181
77
|
end
|
182
|
-
|
@@ -2,11 +2,11 @@ module Capybara
|
|
2
2
|
module Lockstep
|
3
3
|
module Logging
|
4
4
|
def log(message)
|
5
|
-
if debug? && message.present?
|
5
|
+
if Lockstep.debug? && message.present?
|
6
6
|
message = "[capybara-lockstep] #{message}"
|
7
|
-
if is_logger?(
|
8
|
-
# If someone set Capybara::Lockstep to a logger, use that
|
9
|
-
|
7
|
+
if is_logger?(Lockstep.debug)
|
8
|
+
# If someone set Capybara::Lockstep.debug to a logger, use that
|
9
|
+
Lockstep.debug(message)
|
10
10
|
else
|
11
11
|
# Otherwise print to STDOUT
|
12
12
|
puts message
|
@@ -17,8 +17,8 @@ module Capybara
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def is_logger?(object)
|
20
|
-
|
20
|
+
object.respond_to?(:debug)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
24
|
-
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
class Middleware
|
4
|
+
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
tag = "Server request for #{env['PATH_INFO'] || 'unknown path'}"
|
11
|
+
Lockstep.start_work(tag)
|
12
|
+
|
13
|
+
begin
|
14
|
+
@app.call(env)
|
15
|
+
ensure
|
16
|
+
Lockstep.stop_work(tag)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module PageAccess
|
4
|
+
def page
|
5
|
+
Capybara.current_session
|
6
|
+
end
|
7
|
+
|
8
|
+
delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
|
9
|
+
|
10
|
+
def javascript_driver?
|
11
|
+
driver.is_a?(Capybara::Selenium::Driver)
|
12
|
+
end
|
13
|
+
|
14
|
+
def alert_present?
|
15
|
+
# Chrome 54 and/or Chromedriver 2.24 introduced a breaking change on how
|
16
|
+
# accessing browser logs work.
|
17
|
+
#
|
18
|
+
# Apparently, while an alert/confirm is open, Chrome will block any requests
|
19
|
+
# to its `getLog` API. This causes Selenium to time out with a `Net::ReadTimeout` error
|
20
|
+
page.driver.browser.switch_to.alert
|
21
|
+
true
|
22
|
+
rescue Capybara::NotSupportedByDriverError, ::Selenium::WebDriver::Error::NoSuchAlertError
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
class Server
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@job_count = 0
|
8
|
+
@job_count_mutex = Mutex.new
|
9
|
+
@idle_condition = ConditionVariable.new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :job_count
|
13
|
+
|
14
|
+
def start_work(tag)
|
15
|
+
job_count_mutex.synchronize do
|
16
|
+
self.job_count += 1
|
17
|
+
log("Started server work: #{tag} [#{job_count} server jobs]") if tag
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop_work(tag)
|
22
|
+
job_count_mutex.synchronize do
|
23
|
+
self.job_count -= 1
|
24
|
+
log("Stopped server work: #{tag} [#{job_count} server jobs]") if tag
|
25
|
+
|
26
|
+
if job_count == 0
|
27
|
+
idle_condition.broadcast
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def synchronize
|
33
|
+
job_count_mutex.synchronize do
|
34
|
+
if job_count > 0
|
35
|
+
idle_condition.wait(job_count_mutex)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :job_count_mutex, :idle_condition
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module Util
|
4
|
+
class << self
|
5
|
+
def with_max_wait_time(seconds, &block)
|
6
|
+
old_max_wait_time = Capybara.default_max_wait_time
|
7
|
+
Capybara.default_max_wait_time = seconds
|
8
|
+
begin
|
9
|
+
block.call
|
10
|
+
ensure
|
11
|
+
Capybara.default_max_wait_time = old_max_wait_time
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_seconds
|
16
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/capybara-lockstep.rb
CHANGED
@@ -10,8 +10,13 @@ end
|
|
10
10
|
|
11
11
|
require_relative 'capybara-lockstep/version'
|
12
12
|
require_relative 'capybara-lockstep/errors'
|
13
|
+
require_relative 'capybara-lockstep/util'
|
13
14
|
require_relative 'capybara-lockstep/configuration'
|
14
15
|
require_relative 'capybara-lockstep/logging'
|
16
|
+
require_relative 'capybara-lockstep/page_access'
|
15
17
|
require_relative 'capybara-lockstep/lockstep'
|
16
18
|
require_relative 'capybara-lockstep/capybara_ext'
|
17
19
|
require_relative 'capybara-lockstep/helper'
|
20
|
+
require_relative 'capybara-lockstep/server'
|
21
|
+
require_relative 'capybara-lockstep/client'
|
22
|
+
require_relative 'capybara-lockstep/middleware'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capybara-lockstep
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Henning Koch
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: selenium-webdriver
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '4.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '4.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: activesupport
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '4.2'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '4.2'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: ruby2_keywords
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,12 +88,17 @@ files:
|
|
88
88
|
- capybara-lockstep.gemspec
|
89
89
|
- lib/capybara-lockstep.rb
|
90
90
|
- lib/capybara-lockstep/capybara_ext.rb
|
91
|
+
- lib/capybara-lockstep/client.rb
|
91
92
|
- lib/capybara-lockstep/configuration.rb
|
92
93
|
- lib/capybara-lockstep/errors.rb
|
93
94
|
- lib/capybara-lockstep/helper.js
|
94
95
|
- lib/capybara-lockstep/helper.rb
|
95
96
|
- lib/capybara-lockstep/lockstep.rb
|
96
97
|
- lib/capybara-lockstep/logging.rb
|
98
|
+
- lib/capybara-lockstep/middleware.rb
|
99
|
+
- lib/capybara-lockstep/page_access.rb
|
100
|
+
- lib/capybara-lockstep/server.rb
|
101
|
+
- lib/capybara-lockstep/util.rb
|
97
102
|
- lib/capybara-lockstep/version.rb
|
98
103
|
homepage: https://github.com/makandra/capybara-lockstep
|
99
104
|
licenses:
|