capybara-lockstep 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +75 -0
- data/LICENSE.txt +21 -0
- data/README.md +324 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/capybara-lockstep.gemspec +33 -0
- data/lib/capybara-lockstep.rb +17 -0
- data/lib/capybara-lockstep/capybara_ext.rb +71 -0
- data/lib/capybara-lockstep/configuration.rb +41 -0
- data/lib/capybara-lockstep/errors.rb +6 -0
- data/lib/capybara-lockstep/helper.js +205 -0
- data/lib/capybara-lockstep/helper.rb +29 -0
- data/lib/capybara-lockstep/lockstep.rb +141 -0
- data/lib/capybara-lockstep/patiently.rb +58 -0
- data/lib/capybara-lockstep/version.rb +5 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cccdbf8f6eb6582b123a366febeb67f863ac3801cf91f42f7a48f685e96d1860
|
4
|
+
data.tar.gz: a3683b8f821c8dd706eff573fb9c0e5aa730a29dd9205d2441a3ab5a52d8f0b8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e9fc97b54f2b797809c85d45d87b1d34a687ae18617bbf17bdad57d060da96eb876283610e5442cebf876ba520333e501ca635bd48436870359e0b806cfeabc0
|
7
|
+
data.tar.gz: 157ae6e39f58f9437f0121fc3836d2bf3ac0c767b3dff2ff59f5f9b04516fdf4586df04ecc0bbd9c021615551878735edb910fdb393b005cfda59f906832b2f2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.7.2
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
capybara-lockstep (0.2.1)
|
5
|
+
activesupport (>= 3.2)
|
6
|
+
capybara (>= 2.0)
|
7
|
+
selenium-webdriver (>= 3)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activesupport (5.2.4.3)
|
13
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
+
i18n (>= 0.7, < 2)
|
15
|
+
minitest (~> 5.1)
|
16
|
+
tzinfo (~> 1.1)
|
17
|
+
addressable (2.7.0)
|
18
|
+
public_suffix (>= 2.0.2, < 5.0)
|
19
|
+
capybara (3.35.3)
|
20
|
+
addressable
|
21
|
+
mini_mime (>= 0.1.3)
|
22
|
+
nokogiri (~> 1.8)
|
23
|
+
rack (>= 1.6.0)
|
24
|
+
rack-test (>= 0.6.3)
|
25
|
+
regexp_parser (>= 1.5, < 3.0)
|
26
|
+
xpath (~> 3.2)
|
27
|
+
childprocess (3.0.0)
|
28
|
+
concurrent-ruby (1.1.7)
|
29
|
+
diff-lcs (1.3)
|
30
|
+
i18n (1.8.2)
|
31
|
+
concurrent-ruby (~> 1.0)
|
32
|
+
mini_mime (1.0.2)
|
33
|
+
minitest (5.14.1)
|
34
|
+
nokogiri (1.11.1-x86_64-linux)
|
35
|
+
racc (~> 1.4)
|
36
|
+
public_suffix (4.0.6)
|
37
|
+
racc (1.5.2)
|
38
|
+
rack (2.2.3)
|
39
|
+
rack-test (1.1.0)
|
40
|
+
rack (>= 1.0, < 3)
|
41
|
+
rake (13.0.1)
|
42
|
+
regexp_parser (2.1.1)
|
43
|
+
rspec (3.7.0)
|
44
|
+
rspec-core (~> 3.7.0)
|
45
|
+
rspec-expectations (~> 3.7.0)
|
46
|
+
rspec-mocks (~> 3.7.0)
|
47
|
+
rspec-core (3.7.0)
|
48
|
+
rspec-support (~> 3.7.0)
|
49
|
+
rspec-expectations (3.7.0)
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
+
rspec-support (~> 3.7.0)
|
52
|
+
rspec-mocks (3.7.0)
|
53
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
54
|
+
rspec-support (~> 3.7.0)
|
55
|
+
rspec-support (3.7.0)
|
56
|
+
rubyzip (1.3.0)
|
57
|
+
selenium-webdriver (3.142.7)
|
58
|
+
childprocess (>= 0.5, < 4.0)
|
59
|
+
rubyzip (>= 1.2.2)
|
60
|
+
thread_safe (0.3.6)
|
61
|
+
tzinfo (1.2.7)
|
62
|
+
thread_safe (~> 0.1)
|
63
|
+
xpath (3.2.0)
|
64
|
+
nokogiri (~> 1.8)
|
65
|
+
|
66
|
+
PLATFORMS
|
67
|
+
ruby
|
68
|
+
|
69
|
+
DEPENDENCIES
|
70
|
+
capybara-lockstep!
|
71
|
+
rake (~> 13.0)
|
72
|
+
rspec (~> 3.0)
|
73
|
+
|
74
|
+
BUNDLED WITH
|
75
|
+
2.2.12
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Henning Koch
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
# capybara-lockstep
|
2
|
+
|
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 a full-stack integration test suite, even if that suite has timing issues.
|
4
|
+
|
5
|
+
|
6
|
+
Why are tests flaky?
|
7
|
+
--------------------
|
8
|
+
|
9
|
+
A naively written integration test will have [race conditions](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) between the test script and the controlled browser. How often these timing issues will fail your test depends on luck and your machine's performance. You may not see these issues for years until a colleague runs your suite on their new laptop.
|
10
|
+
|
11
|
+
Here is a typical example for a test that will fail with unlucky timing:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
scenario 'User sends a tweet' do
|
15
|
+
visit '/'
|
16
|
+
click_link 'New tweet' # opens form in a modal dialog
|
17
|
+
fill_in 'text', with: 'My first tweet'
|
18
|
+
click_button 'Send tweet'
|
19
|
+
visit '/timeline'
|
20
|
+
expect(page).to have_css('.tweet', text: 'My first tweet')
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
This test has four timing issues that may cause it to fail:
|
25
|
+
|
26
|
+
1. We click on the "New tweet" button, but the the JS event handler to open the tweet form wasn't registered yet.
|
27
|
+
2. We start filling in the form, but it wasn't loaded yet.
|
28
|
+
3. After sending the tweet we immediately navigate away, killing the form submission request that is still in flight. Hence the tweet will never appear in the next step.
|
29
|
+
4. We look for the new tweet, but the timeline wasn't loaded yet.
|
30
|
+
|
31
|
+
Capybara will retry individual commands or expectations when they fail. However, only issues **2** and **4** can be healed by retrying.
|
32
|
+
|
33
|
+
While it is [possible](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) to remove most of the timing issues above, it requires skill and discipline. capybara-lockstep fixes issues **1**, **2**, **3** and **4** without any changes to the test code.
|
34
|
+
|
35
|
+
|
36
|
+
How capybara-lockstep helps
|
37
|
+
---------------------------
|
38
|
+
|
39
|
+
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.
|
40
|
+
|
41
|
+
Whenever Capybara visits a new URL:
|
42
|
+
|
43
|
+
- capybara-lockstep waits for all document resources to load.
|
44
|
+
- capybara-lockstep waits for client-side JavaScript to render or hydrate DOM elements.
|
45
|
+
- capybara-lockstep waits for any AJAX requests.
|
46
|
+
- capybara-lockstep waits for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
|
47
|
+
|
48
|
+
Whenever Capybara simulates a user interaction (clicking, typing, etc.):
|
49
|
+
|
50
|
+
- capybara-lockstep waits for any AJAX requests.
|
51
|
+
- capybara-lockstep waits for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
|
52
|
+
|
53
|
+
|
54
|
+
Installation
|
55
|
+
------------
|
56
|
+
|
57
|
+
### Prerequisites
|
58
|
+
|
59
|
+
Check if your application satisfies all requirements for capybara-lockstep:
|
60
|
+
|
61
|
+
- Capybara 2 or higher.
|
62
|
+
- Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-headless deactivates itself for any other driver.
|
63
|
+
- 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.
|
64
|
+
- This gem was only tested with Rails, but there's no Rails dependency.
|
65
|
+
|
66
|
+
|
67
|
+
### Installing the Ruby gem
|
68
|
+
|
69
|
+
Assuming that you're using Rails Add this line to your application's `Gemfile`:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
group :test do
|
73
|
+
gem 'capybara-lockstep'
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
And then execute:
|
78
|
+
|
79
|
+
```bash
|
80
|
+
$ bundle install
|
81
|
+
```
|
82
|
+
|
83
|
+
If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
|
84
|
+
|
85
|
+
|
86
|
+
### Including the JavaScript snippet
|
87
|
+
|
88
|
+
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.
|
89
|
+
|
90
|
+
If you're using Rails you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
|
91
|
+
|
92
|
+
```erb
|
93
|
+
<%= capybara_lockstep if Rails.env.test? %>
|
94
|
+
```
|
95
|
+
|
96
|
+
Ideally the snippet should be included in the `<head>` before any other `<script>` tags. If that's impractical you will also see some benefit if you insert it later.
|
97
|
+
|
98
|
+
If you have a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), the `capybara_lockstep` helper will insert a CSP nonce by default. You can also pass a `:nonce` option.
|
99
|
+
|
100
|
+
If you're not using Rails you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
|
101
|
+
|
102
|
+
|
103
|
+
### Signaling the end of page initialization
|
104
|
+
|
105
|
+
Most web applications run some JavaScript after the document was loaded. This JavaScript enhances existing DOM elements ("hydration") or renders additional element into the DOM.
|
106
|
+
|
107
|
+
capybara-lockstep needs to know when your JavaScript is done hydrating and rendering, so it can automatically wait for initialization after every Capybara `visit()`.
|
108
|
+
|
109
|
+
To signal that JavaScript is still initializing, your application layouts should render the `<body>` element with an `[data-initializing]` attribute:
|
110
|
+
|
111
|
+
```html
|
112
|
+
<body data-initializing>
|
113
|
+
```
|
114
|
+
|
115
|
+
Your application JavaScript should remove the `[data-initializing]` attribute when it is done hydrating and rendering.
|
116
|
+
|
117
|
+
More precisely, the attribute should be removed in the same [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) ("tick") that will finish initializing. capybara-lockstep will assume that the page will be initialized by the end of this task.
|
118
|
+
|
119
|
+
If all your initializing JavaScript runs synchronously on `DOMContentLoaded`, you can remove `[data-initializing]` in an event handler:
|
120
|
+
|
121
|
+
```js
|
122
|
+
document.addEventListener('DOMContentLoaded', function() {
|
123
|
+
// Initialize the page here
|
124
|
+
document.body.removeAttribute('data-initializing')
|
125
|
+
})
|
126
|
+
```
|
127
|
+
|
128
|
+
If you do any asynchronous initialization work (like lazy-loading another script) you should only remove `[data-initializing]` once that is done:
|
129
|
+
|
130
|
+
```js
|
131
|
+
document.addEventListener('DOMContentLoaded', function() {
|
132
|
+
import('huge-library').then(function({ hugeLibrary }) {
|
133
|
+
hugeLibrary.initialize()
|
134
|
+
document.body.removeAttribute('data-initializing')
|
135
|
+
})
|
136
|
+
})
|
137
|
+
```
|
138
|
+
|
139
|
+
If you call libraries during initialization, you may need to check the library code to see whether it finishes synchronously or asynchronously. E.g. if you discover that a library delays work for a task, you must also wait another task to remove `[data-initializing]`:
|
140
|
+
|
141
|
+
```js
|
142
|
+
document.addEventListener('DOMContentLoaded', function() {
|
143
|
+
Libary.doWorkInNextTask()
|
144
|
+
setTimeout(function() { document.body.removeAttribute('data-initializing') })
|
145
|
+
})
|
146
|
+
```
|
147
|
+
|
148
|
+
When you're using [Unpoly](https://unpoly.com/) initializing will usually happen synchronously in [compilers](https://unpoly.com/up.compiler). Hence a compiler is a good place to remove `[data-initializing]`:
|
149
|
+
|
150
|
+
```js
|
151
|
+
up.compiler('body', function(body) {
|
152
|
+
body.removeAttribute('data-initializing')
|
153
|
+
})
|
154
|
+
```
|
155
|
+
|
156
|
+
When you're using [AngularJS 1](https://unpoly.com/) initializing will usually happen synchronously in [directives](https://docs.angularjs.org/guide/directive). Hence a directive is a good place to remove `[data-initializing]`:
|
157
|
+
|
158
|
+
```js
|
159
|
+
app.directive('body', function() {
|
160
|
+
return {
|
161
|
+
restrict: 'E',
|
162
|
+
link: function() {
|
163
|
+
document.body.removeAttribute('data-initializing')
|
164
|
+
}
|
165
|
+
}
|
166
|
+
})
|
167
|
+
```
|
168
|
+
|
169
|
+
### Verify successful integration
|
170
|
+
|
171
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
|
172
|
+
|
173
|
+
Run your test suite to see if integration was successful and whether stability improves.
|
174
|
+
|
175
|
+
When you run into issues or don't see an effect, try activating `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
|
176
|
+
|
177
|
+
Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
|
178
|
+
|
179
|
+
|
180
|
+
## Performance impact
|
181
|
+
|
182
|
+
capybara-lockstep may or may not impact the runtime of your test suite. It depends on your particular tests and how many flaky tests you're seeing in the first place.
|
183
|
+
|
184
|
+
While waiting for the browser to be idle does take a few milliseconds, Capybara no longer needs to retry failed commands. You will also save time from not needing to re-run failed tests.
|
185
|
+
|
186
|
+
In casual testing I experienced a negative performance impact between 0% and 10%.
|
187
|
+
|
188
|
+
|
189
|
+
## Debugging log
|
190
|
+
|
191
|
+
capybara-lockstep can print to the console whenever it waits for the browser. To enable the log:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
Capybara::Lockstep.debug = true
|
195
|
+
```
|
196
|
+
|
197
|
+
You should now see messages like this during your test runs:
|
198
|
+
|
199
|
+
```
|
200
|
+
[Capybara::Lockstep] JavaScript or AJAX requests are running
|
201
|
+
```
|
202
|
+
|
203
|
+
You may also configure logging to an existing logger object:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
Capybara::Lockstep.debug = Rails.logger
|
207
|
+
```
|
208
|
+
|
209
|
+
|
210
|
+
## Disabling synchronization
|
211
|
+
|
212
|
+
If for some reason you want to disable browser synchronization for a while, you can do it like this:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
begin
|
216
|
+
Capybara::Lockstep.enabled = false
|
217
|
+
do_unsynchronized_work
|
218
|
+
ensure
|
219
|
+
Capybara::Lockstep.enabled = true
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
|
224
|
+
## Timeout
|
225
|
+
|
226
|
+
By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
|
227
|
+
|
228
|
+
You can configure a different timeout:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Capybara::Lockstep.timeout = 5 # seconds
|
232
|
+
```
|
233
|
+
|
234
|
+
|
235
|
+
|
236
|
+
|
237
|
+
## JavaScript API
|
238
|
+
|
239
|
+
capybara-lockstep already hooks into [many JavaScript APIs](#how-capybara-lockstep-helps) like `XMLHttpRequest` or `fetch()` to mark the browser as "busy" until their work finishes. **This should be enough for most test suites**.
|
240
|
+
|
241
|
+
For additional edge cases you may interact with capybara-lockstep from your own JavaScripts.
|
242
|
+
|
243
|
+
Note that when you only load the JavaScript snippet in tests you need check before calling any API functions:
|
244
|
+
|
245
|
+
```js
|
246
|
+
if (window.CapybaraLockstep) {
|
247
|
+
CapybaraLockstep.startWork()
|
248
|
+
}
|
249
|
+
```
|
250
|
+
|
251
|
+
### Signaling asynchronous work
|
252
|
+
|
253
|
+
If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
|
254
|
+
|
255
|
+
```js
|
256
|
+
CapybaraLockstep.startWork()
|
257
|
+
doAsynchronousWork().then(function() {
|
258
|
+
CapybaraLockstep.stopWork()
|
259
|
+
})
|
260
|
+
```
|
261
|
+
|
262
|
+
### Checking if the browser is busy
|
263
|
+
|
264
|
+
You can query capybara-lockstep whether it considers the browser to be busy or idle:
|
265
|
+
|
266
|
+
```js
|
267
|
+
CapybaraLockstep.isBusy() // => false
|
268
|
+
CapybaraLockstep.isIdle() // => true
|
269
|
+
```
|
270
|
+
|
271
|
+
### Waiting until the browser is idle
|
272
|
+
|
273
|
+
```js
|
274
|
+
CapybaraLockstep.awaitIdle(callback)
|
275
|
+
```
|
276
|
+
|
277
|
+
## Ruby API
|
278
|
+
|
279
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
|
280
|
+
|
281
|
+
For additional edge cases you may interact with capybara-lockstep from your Ruby code.
|
282
|
+
|
283
|
+
|
284
|
+
### Waiting until the browser is idle
|
285
|
+
|
286
|
+
This will block until the document was loaded and the DOM has been hydrated:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
Capybara::Lockstep.await_initialized
|
290
|
+
```
|
291
|
+
|
292
|
+
This will block while the browser is busy with JavaScript and AJAX requests:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
Capybara::Lockstep.await_idle
|
296
|
+
```
|
297
|
+
|
298
|
+
### Checking if the browser is busy
|
299
|
+
|
300
|
+
You can query capybara-lockstep whether it considers the browser to be busy or idle:
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
Capybara::Lockstep.idle? # => true
|
304
|
+
Capybara::Lockstep.busy? # => false
|
305
|
+
```
|
306
|
+
|
307
|
+
|
308
|
+
## Development
|
309
|
+
|
310
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
311
|
+
|
312
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
313
|
+
|
314
|
+
## Contributing
|
315
|
+
|
316
|
+
Pull requests are welcome on GitHub at <https://github.com/makandra/capistrano-lockstep>.
|
317
|
+
|
318
|
+
## License
|
319
|
+
|
320
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
321
|
+
|
322
|
+
## Credits
|
323
|
+
|
324
|
+
Henning Koch ([@triskweline](https://twitter.com/triskweline)) from [makandra](https://makandra.com).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "capybara-lockstep"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "lib/capybara-lockstep/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "capybara-lockstep"
|
5
|
+
spec.version = Capybara::Lockstep::VERSION
|
6
|
+
spec.authors = ["Henning Koch"]
|
7
|
+
spec.email = ["henning.koch@makandra.de"]
|
8
|
+
|
9
|
+
spec.summary = "Synchronize Capybara commands with client-side JavaScript and AJAX requests"
|
10
|
+
spec.homepage = "https://rubygems.org/gems/capybara-lockstep"
|
11
|
+
spec.license = "MIT"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
# Uncomment to register a new dependency of your gem
|
27
|
+
spec.add_dependency "capybara", ">= 2.0"
|
28
|
+
spec.add_dependency "selenium-webdriver", ">= 3"
|
29
|
+
spec.add_dependency "activesupport", ">= 3.2"
|
30
|
+
|
31
|
+
# For more information and examples about making a new gem, checkout our
|
32
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'capybara'
|
2
|
+
require 'selenium-webdriver'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
6
|
+
module Capybara
|
7
|
+
module Lockstep
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'capybara-lockstep/version'
|
12
|
+
require_relative 'capybara-lockstep/errors'
|
13
|
+
require_relative 'capybara-lockstep/patiently'
|
14
|
+
require_relative 'capybara-lockstep/configuration'
|
15
|
+
require_relative 'capybara-lockstep/lockstep'
|
16
|
+
require_relative 'capybara-lockstep/capybara_ext'
|
17
|
+
require_relative 'capybara-lockstep/helper'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module VisitWithWaiting
|
4
|
+
def visit(*args, **kwargs, &block)
|
5
|
+
super(*args, **kwargs, &block).tap do
|
6
|
+
# There is a step that changes drivers mid-scenario.
|
7
|
+
# It works by creating a new Capybara session and re-visits the
|
8
|
+
# URL from the previous session. If this happens before a URL is ever
|
9
|
+
# loaded, it re-visits the URL "data:", which will never "finish"
|
10
|
+
# initializing.
|
11
|
+
unless args[0].start_with?('data:')
|
12
|
+
Capybara::Lockstep.await_initialized
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module AwaitIdle
|
19
|
+
def await_idle(meth)
|
20
|
+
mod = Module.new do
|
21
|
+
define_method meth do |*args, &block|
|
22
|
+
super(*args, &block).tap do
|
23
|
+
Capybara::Lockstep.await_idle
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
prepend(mod)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Capybara::Session.class_eval do
|
34
|
+
prepend Capybara::Lockstep::VisitWithWaiting
|
35
|
+
end
|
36
|
+
|
37
|
+
# Capybara 3 has driver-specific Node classes which sometimes
|
38
|
+
# super to Capybara::Selenium::Node, but not always.
|
39
|
+
node_classes = [
|
40
|
+
(Capybara::Selenium::ChromeNode if defined?(Capybara::Selenium::ChromeNode)),
|
41
|
+
(Capybara::Selenium::FirefoxNode if defined?(Capybara::Selenium::FirefoxNode)),
|
42
|
+
(Capybara::Selenium::SafariNode if defined?(Capybara::Selenium::SafariNode)),
|
43
|
+
(Capybara::Selenium::EdgeNode if defined?(Capybara::Selenium::EdgeNode)),
|
44
|
+
(Capybara::Selenium::IENode if defined?(Capybara::Selenium::IENode)),
|
45
|
+
].compact
|
46
|
+
|
47
|
+
if node_classes.empty?
|
48
|
+
# Capybara 2 has no driver-specific Node implementations,
|
49
|
+
# so we patch the shared base class.
|
50
|
+
node_classes = [Capybara::Selenium::Node]
|
51
|
+
end
|
52
|
+
|
53
|
+
node_classes.each do |node_class|
|
54
|
+
node_class.class_eval do
|
55
|
+
extend Capybara::Lockstep::AwaitIdle
|
56
|
+
|
57
|
+
await_idle :set
|
58
|
+
await_idle :select_option
|
59
|
+
await_idle :unselect_option
|
60
|
+
await_idle :click
|
61
|
+
await_idle :right_click
|
62
|
+
await_idle :double_click
|
63
|
+
await_idle :send_keys
|
64
|
+
await_idle :hover
|
65
|
+
await_idle :drag_to
|
66
|
+
await_idle :drop
|
67
|
+
await_idle :scroll_by
|
68
|
+
await_idle :scroll_to
|
69
|
+
await_idle :trigger
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module Configuration
|
4
|
+
|
5
|
+
def timeout
|
6
|
+
@timeout || 10
|
7
|
+
end
|
8
|
+
|
9
|
+
def timeout=(seconds)
|
10
|
+
@timeout = seconds
|
11
|
+
end
|
12
|
+
|
13
|
+
def debug?
|
14
|
+
@debug.nil? ? false : @debug
|
15
|
+
end
|
16
|
+
|
17
|
+
def debug=(debug)
|
18
|
+
@debug = debug
|
19
|
+
end
|
20
|
+
|
21
|
+
def enabled?
|
22
|
+
if javascript_driver?
|
23
|
+
@enabled.nil? ? true : @enabled
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def enabled=(enabled)
|
30
|
+
@enabled = enabled
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def javascript_driver?
|
36
|
+
driver.is_a?(Capybara::Selenium::Driver)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
window.CapybaraLockstep = (function() {
|
2
|
+
var count = 0
|
3
|
+
var idleCallbacks = []
|
4
|
+
|
5
|
+
function isIdle() {
|
6
|
+
// Can't check for document.readyState or body.initializing here,
|
7
|
+
// since the user might navigate away from the page before it finishes
|
8
|
+
// initializing.
|
9
|
+
return count === 0
|
10
|
+
}
|
11
|
+
|
12
|
+
function isBusy() {
|
13
|
+
return !isIdle()
|
14
|
+
}
|
15
|
+
|
16
|
+
function startWork() {
|
17
|
+
count++
|
18
|
+
}
|
19
|
+
|
20
|
+
function startWorkUntil(promise) {
|
21
|
+
startWork()
|
22
|
+
promise.then(stopWork, stopWork)
|
23
|
+
}
|
24
|
+
|
25
|
+
function startWorkForTime(time) {
|
26
|
+
startWork()
|
27
|
+
setTimeout(stopWork, time)
|
28
|
+
}
|
29
|
+
|
30
|
+
function startWorkForMicrotask() {
|
31
|
+
startWork()
|
32
|
+
Promise.resolve().then(stopWork)
|
33
|
+
}
|
34
|
+
|
35
|
+
function stopWork() {
|
36
|
+
count--
|
37
|
+
|
38
|
+
if (isIdle()) {
|
39
|
+
idleCallbacks.forEach(function(callback) {
|
40
|
+
callback('JavaScript has finished')
|
41
|
+
})
|
42
|
+
idleCallbacks = []
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
function trackFetch() {
|
47
|
+
if (!window.fetch) {
|
48
|
+
return
|
49
|
+
}
|
50
|
+
|
51
|
+
var oldFetch = window.fetch
|
52
|
+
window.fetch = function() {
|
53
|
+
var promise = oldFetch.apply(this, arguments)
|
54
|
+
startWorkUntil(promise)
|
55
|
+
return promise
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
function trackXHR() {
|
60
|
+
var oldSend = XMLHttpRequest.prototype.send
|
61
|
+
|
62
|
+
XMLHttpRequest.prototype.send = function() {
|
63
|
+
startWork()
|
64
|
+
|
65
|
+
try {
|
66
|
+
this.addEventListener('readystatechange', function(event) {
|
67
|
+
if (this.readyState === 4) { stopWork() }
|
68
|
+
}.bind(this))
|
69
|
+
return oldSend.apply(this, arguments)
|
70
|
+
} catch (e) {
|
71
|
+
// If we get a sync exception during request dispatch
|
72
|
+
// we assume the request never went out.
|
73
|
+
stopWork()
|
74
|
+
throw e
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
function trackInteraction() {
|
80
|
+
// We already override all interaction methods in the Selenium browser nodes, so they
|
81
|
+
// wait for an idle frame afterwards. However a test script might also dispatch synthetic
|
82
|
+
// events with executate_script() to manipulate the browser in ways that are not possible
|
83
|
+
// with the Capybara API. When we observe such an event we wait until the end of the microtask,
|
84
|
+
// assuming any busy action will be queued by then.
|
85
|
+
['click', 'mousedown', 'keydown', 'change', 'input', 'submit', 'focusin', 'focusout', 'scroll'].forEach(function(eventType) {
|
86
|
+
// Use { useCapture: true } so we get the event before another listener
|
87
|
+
// can prevent it from bubbling up to the document.
|
88
|
+
document.addEventListener(eventType, onInteraction, { capture: true, passive: true })
|
89
|
+
})
|
90
|
+
}
|
91
|
+
|
92
|
+
function onInteraction() {
|
93
|
+
// We wait until the end of this microtask, assuming that any callback that
|
94
|
+
// would queue an AJAX request or load additional scripts will run by then.
|
95
|
+
startWorkForMicrotask()
|
96
|
+
}
|
97
|
+
|
98
|
+
function trackHistory() {
|
99
|
+
['popstate'].forEach(function(eventType) {
|
100
|
+
document.addEventListener(eventType, onHistoryEvent)
|
101
|
+
})
|
102
|
+
}
|
103
|
+
|
104
|
+
function onHistoryEvent() {
|
105
|
+
// After calling history.back() or history.forward() the popstate event will *not*
|
106
|
+
// fire synchronously. It will also not fire in the next task. Chrome sometimes fires
|
107
|
+
// it after 10ms, but sometimes it takes longer.
|
108
|
+
startWorkForTime(100)
|
109
|
+
}
|
110
|
+
|
111
|
+
function trackDynamicScripts() {
|
112
|
+
if (!window.MutationObserver) {
|
113
|
+
return
|
114
|
+
}
|
115
|
+
|
116
|
+
// Dynamic imports or analytics snippets may insert a <script src>
|
117
|
+
// tag that loads and executes additional JavaScript. We want to be isBusy()
|
118
|
+
// until such scripts have loaded or errored.
|
119
|
+
var observer = new MutationObserver(onMutated)
|
120
|
+
observer.observe(document, { subtree: true, childList: true })
|
121
|
+
}
|
122
|
+
|
123
|
+
function trackJQuery() {
|
124
|
+
// jQuery may be loaded after us, so we wait until DOMContentReady.
|
125
|
+
whenReady(function() {
|
126
|
+
if (!window.jQuery) {
|
127
|
+
return
|
128
|
+
}
|
129
|
+
|
130
|
+
// Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
|
131
|
+
// not resolve in the next microtask but in the next *task* (it makes itself
|
132
|
+
// async using setTimoeut()). Hence we need to wait for it in addition to XHR.
|
133
|
+
var oldAjax = jQuery.ajax
|
134
|
+
jQuery.ajax = function () {
|
135
|
+
var promise = oldAjax.apply(this, arguments)
|
136
|
+
startWorkUntil(promise)
|
137
|
+
return promise
|
138
|
+
}
|
139
|
+
})
|
140
|
+
}
|
141
|
+
|
142
|
+
function isRemoteScript(node) {
|
143
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
|
144
|
+
var src = node.getAttribute('src')
|
145
|
+
var type = node.getAttribute('type')
|
146
|
+
|
147
|
+
return (src && (!type || /javascript/i.test(type)))
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
function onRemoteScriptAdded(script) {
|
152
|
+
startWork()
|
153
|
+
// Chrome runs a remote <script> *before* the load event fires.
|
154
|
+
script.addEventListener('load', stopWork)
|
155
|
+
script.addEventListener('error', stopWork)
|
156
|
+
}
|
157
|
+
|
158
|
+
function onMutated(changes) {
|
159
|
+
changes.forEach(function(change) {
|
160
|
+
change.addedNodes.forEach(function(addedNode) {
|
161
|
+
if (isRemoteScript(addedNode)) {
|
162
|
+
onRemoteScriptAdded(addedNode)
|
163
|
+
}
|
164
|
+
})
|
165
|
+
})
|
166
|
+
}
|
167
|
+
|
168
|
+
function whenReady(callback) {
|
169
|
+
// Values are "loading", "interactive" and "completed".
|
170
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
|
171
|
+
if (document.readyState != 'loading') {
|
172
|
+
callback()
|
173
|
+
} else {
|
174
|
+
document.addEventListener('DOMContentLoaded', callback)
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
function track() {
|
179
|
+
trackFetch()
|
180
|
+
trackXHR()
|
181
|
+
trackInteraction()
|
182
|
+
trackHistory()
|
183
|
+
trackDynamicScripts()
|
184
|
+
trackJQuery()
|
185
|
+
}
|
186
|
+
|
187
|
+
function awaitIdle(callback) {
|
188
|
+
if (isIdle()) {
|
189
|
+
callback()
|
190
|
+
} else {
|
191
|
+
idleCallbacks.push(callback)
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
return {
|
196
|
+
track: track,
|
197
|
+
startWork: startWork,
|
198
|
+
stopWork: stopWork,
|
199
|
+
awaitIdle: awaitIdle,
|
200
|
+
isIdle: isIdle,
|
201
|
+
isBusy: isBusy
|
202
|
+
}
|
203
|
+
})()
|
204
|
+
|
205
|
+
CapybaraLockstep.track()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module Helper
|
4
|
+
|
5
|
+
JS_PATH = File.expand_path('../helper.js', __FILE__)
|
6
|
+
JS = IO.read(JS_PATH)
|
7
|
+
|
8
|
+
def capybara_lockstep_js
|
9
|
+
JS
|
10
|
+
end
|
11
|
+
|
12
|
+
def capybara_lockstep(options = {})
|
13
|
+
tag_options = {}
|
14
|
+
|
15
|
+
# Add a CSRF nonce if supported by our Rails version
|
16
|
+
if Rails.version >= '5'
|
17
|
+
tag_options[:nonce] = options.fetch(:nonce, true)
|
18
|
+
end
|
19
|
+
|
20
|
+
javascript_tag(capybara_lockstep_js, tag_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if defined?(ActionView::Base)
|
28
|
+
ActionView::Base.send :include, Capybara::Lockstep::Helper
|
29
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
class << self
|
4
|
+
include Patiently
|
5
|
+
include Configuration
|
6
|
+
|
7
|
+
def await_idle
|
8
|
+
return unless enabled?
|
9
|
+
|
10
|
+
ignoring_alerts do
|
11
|
+
# evaluate_async_script also times out after Capybara.default_max_wait_time
|
12
|
+
with_max_wait_time(timeout) do
|
13
|
+
message_from_js = evaluate_async_script(<<~JS)
|
14
|
+
let done = arguments[0]
|
15
|
+
if (window.CapybaraLockstep) {
|
16
|
+
CapybaraLockstep.awaitIdle(done)
|
17
|
+
} else {
|
18
|
+
done('Cannot synchronize: Capybara::Lockstep was not included in page')
|
19
|
+
}
|
20
|
+
JS
|
21
|
+
log(message_from_js)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def await_initialized
|
27
|
+
return unless enabled?
|
28
|
+
|
29
|
+
# We're retrying the initialize check every few ms.
|
30
|
+
# Don't clutter the log with dozens of identical messages.
|
31
|
+
last_logged_reason = nil
|
32
|
+
|
33
|
+
patiently(timeout) do
|
34
|
+
if (reason = initialize_reason)
|
35
|
+
if reason != last_logged_reason
|
36
|
+
log(reason)
|
37
|
+
last_logged_reason = reason
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raise an exception that will be retried by `patiently`
|
41
|
+
raise Busy, reason
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def idle?
|
47
|
+
unless enabled?
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
|
51
|
+
result = execute_script(<<~JS)
|
52
|
+
if (window.CapybaraLockstep) {
|
53
|
+
return CapybaraLockstep.isIdle()
|
54
|
+
} else {
|
55
|
+
return 'Cannot check busy state: Capybara::Lockstep was not included in page'
|
56
|
+
}
|
57
|
+
JS
|
58
|
+
|
59
|
+
if result.is_a?(String)
|
60
|
+
log(result)
|
61
|
+
# When the snippet is missing we assume that the browser is idle.
|
62
|
+
# Otherwise we would wait forever.
|
63
|
+
true
|
64
|
+
else
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def busy?
|
70
|
+
!idle?
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def initialize_reason
|
76
|
+
ignoring_alerts do
|
77
|
+
execute_script(<<~JS)
|
78
|
+
if (location.href.indexOf('data:') == 0) {
|
79
|
+
return 'Requesting initial page'
|
80
|
+
}
|
81
|
+
|
82
|
+
if (document.readyState !== "complete") {
|
83
|
+
return 'Document is loading'
|
84
|
+
}
|
85
|
+
|
86
|
+
// The application layouts render a <body data-initializing>.
|
87
|
+
// The [data-initializing] attribute is removed by an Angular directive or Unpoly compiler (frontend).
|
88
|
+
// to signal that all elements have been activated.
|
89
|
+
if (document.querySelector('body[data-initializing]')) {
|
90
|
+
return 'DOM is being hydrated'
|
91
|
+
}
|
92
|
+
|
93
|
+
if (window.CapybaraLockstep && CapybaraLockstep.isBusy()) {
|
94
|
+
return 'JavaScript or AJAX requests are running'
|
95
|
+
}
|
96
|
+
|
97
|
+
return false
|
98
|
+
JS
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def page
|
103
|
+
Capybara.current_session
|
104
|
+
end
|
105
|
+
|
106
|
+
delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
|
107
|
+
|
108
|
+
def ignoring_alerts(&block)
|
109
|
+
block.call
|
110
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
111
|
+
# noop
|
112
|
+
end
|
113
|
+
|
114
|
+
def with_max_wait_time(seconds, &block)
|
115
|
+
old_max_wait_time = Capybara.default_max_wait_time
|
116
|
+
Capybara.default_max_wait_time = seconds
|
117
|
+
begin
|
118
|
+
block.call
|
119
|
+
ensure
|
120
|
+
Capybara.default_max_wait_time = old_max_wait_time
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def log(message)
|
125
|
+
if debug? && message.present?
|
126
|
+
message = "[Capybara::Lockstep] #{message}"
|
127
|
+
if @debug.respond_to?(:debug)
|
128
|
+
# If someone set Capybara::Lockstep to a logger, use that
|
129
|
+
@debug.debug(message)
|
130
|
+
else
|
131
|
+
# Otherwise print to STDOUT
|
132
|
+
puts message
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
# Ported from https://github.com/makandra/spreewald/blob/master/lib/spreewald_support/tolerance_for_selenium_sync_issues.rb
|
4
|
+
module Patiently
|
5
|
+
|
6
|
+
RETRY_ERRORS = %w[
|
7
|
+
Capybara::Lockstep::Busy
|
8
|
+
Capybara::ElementNotFound
|
9
|
+
Spec::Expectations::ExpectationNotMetError
|
10
|
+
RSpec::Expectations::ExpectationNotMetError
|
11
|
+
Minitest::Assertion
|
12
|
+
Capybara::Poltergeist::ClickFailed
|
13
|
+
Capybara::ExpectationNotMet
|
14
|
+
Selenium::WebDriver::Error::StaleElementReferenceError
|
15
|
+
Selenium::WebDriver::Error::NoAlertPresentError
|
16
|
+
Selenium::WebDriver::Error::ElementNotVisibleError
|
17
|
+
Selenium::WebDriver::Error::NoSuchFrameError
|
18
|
+
Selenium::WebDriver::Error::NoAlertPresentError
|
19
|
+
Selenium::WebDriver::Error::JavascriptError
|
20
|
+
Selenium::WebDriver::Error::UnknownError
|
21
|
+
Selenium::WebDriver::Error::NoSuchAlertError
|
22
|
+
]
|
23
|
+
|
24
|
+
# evaluate_script latency is ~ 0.025s
|
25
|
+
WAIT_PERIOD = 0.03
|
26
|
+
|
27
|
+
def patiently(timeout = Capybara.default_max_wait_time, &block)
|
28
|
+
started = monotonic_time
|
29
|
+
tries = 0
|
30
|
+
begin
|
31
|
+
tries += 1
|
32
|
+
block.call
|
33
|
+
rescue Exception => e
|
34
|
+
raise e unless retryable_error?(e)
|
35
|
+
raise e if (monotonic_time - started > timeout && tries >= 2)
|
36
|
+
sleep(WAIT_PERIOD)
|
37
|
+
if monotonic_time == started
|
38
|
+
raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead"
|
39
|
+
end
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def monotonic_time
|
47
|
+
# We use the system clock (i.e. seconds since boot) to calculate the time,
|
48
|
+
# because Time.now may be frozen
|
49
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
50
|
+
end
|
51
|
+
|
52
|
+
def retryable_error?(e)
|
53
|
+
RETRY_ERRORS.include?(e.class.name)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: capybara-lockstep
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henning Koch
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: capybara
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: selenium-webdriver
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- henning.koch@makandra.de
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".ruby-version"
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- bin/console
|
71
|
+
- bin/setup
|
72
|
+
- capybara-lockstep.gemspec
|
73
|
+
- lib/capybara-lockstep.rb
|
74
|
+
- lib/capybara-lockstep/capybara_ext.rb
|
75
|
+
- lib/capybara-lockstep/configuration.rb
|
76
|
+
- lib/capybara-lockstep/errors.rb
|
77
|
+
- lib/capybara-lockstep/helper.js
|
78
|
+
- lib/capybara-lockstep/helper.rb
|
79
|
+
- lib/capybara-lockstep/lockstep.rb
|
80
|
+
- lib/capybara-lockstep/patiently.rb
|
81
|
+
- lib/capybara-lockstep/version.rb
|
82
|
+
homepage: https://rubygems.org/gems/capybara-lockstep
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata:
|
86
|
+
homepage_uri: https://rubygems.org/gems/capybara-lockstep
|
87
|
+
source_code_uri: https://rubygems.org/gems/capybara-lockstep
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.4.0
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.2.6
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Synchronize Capybara commands with client-side JavaScript and AJAX requests
|
107
|
+
test_files: []
|