capybara-lockstep 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -1
- data/README.md +59 -55
- data/lib/capybara-lockstep.rb +1 -1
- data/lib/capybara-lockstep/capybara_ext.rb +33 -27
- data/lib/capybara-lockstep/configuration.rb +26 -2
- data/lib/capybara-lockstep/helper.js +92 -52
- data/lib/capybara-lockstep/helper.rb +7 -1
- data/lib/capybara-lockstep/lockstep.rb +88 -121
- data/lib/capybara-lockstep/logging.rb +24 -0
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +3 -3
- data/lib/capybara-lockstep/patiently.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 783200fa3cbde0166b11c213bb677250021185b7c9898a3fd95594dd8433b89a
|
4
|
+
data.tar.gz: 77efe617f3ce7f138da78e8b9db031b9a8f75f07d3afe8379ae903266a9fa389
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ec80d343f193b6b5a166bae1bf5ab6be2e7ae2e3c82d903596e4c604985d96382cce5012df5abdb714da614d8741f062386d60a47a1a3340734c22e46814949
|
7
|
+
data.tar.gz: 02e870bc8b8377be23e0ec97086219c7fd75a098f6f4eeda953dac86b7eb3a957d93984fb26265e8322c21989daa28cf4713ee3dc1772cd652f493e7999b6b57
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
capybara-lockstep (0.
|
4
|
+
capybara-lockstep (0.4.0)
|
5
5
|
activesupport (>= 3.2)
|
6
6
|
capybara (>= 2.0)
|
7
7
|
selenium-webdriver (>= 3)
|
@@ -17,6 +17,7 @@ GEM
|
|
17
17
|
zeitwerk (~> 2.3)
|
18
18
|
addressable (2.7.0)
|
19
19
|
public_suffix (>= 2.0.2, < 5.0)
|
20
|
+
byebug (11.1.3)
|
20
21
|
capybara (3.35.3)
|
21
22
|
addressable
|
22
23
|
mini_mime (>= 0.1.3)
|
@@ -70,6 +71,7 @@ PLATFORMS
|
|
70
71
|
ruby
|
71
72
|
|
72
73
|
DEPENDENCIES
|
74
|
+
byebug
|
73
75
|
capybara-lockstep!
|
74
76
|
rake (~> 13.0)
|
75
77
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -170,9 +170,13 @@ app.directive('body', function() {
|
|
170
170
|
|
171
171
|
capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
|
172
172
|
|
173
|
-
Run your test suite to see if integration was successful and whether stability improves.
|
173
|
+
Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber). You should see messages like this in your console:
|
174
174
|
|
175
|
-
|
175
|
+
```text
|
176
|
+
[capybara-lockstep] Synchronizing
|
177
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
178
|
+
[capybara-lockstep] Synchronized successfully
|
179
|
+
```
|
176
180
|
|
177
181
|
Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
|
178
182
|
|
@@ -183,23 +187,37 @@ capybara-lockstep may or may not impact the runtime of your test suite. It depen
|
|
183
187
|
|
184
188
|
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
189
|
|
186
|
-
In casual testing I experienced a
|
190
|
+
In casual testing I experienced a performance impact between +/- 10%.
|
187
191
|
|
188
192
|
|
189
193
|
## Debugging log
|
190
194
|
|
191
|
-
capybara-lockstep
|
195
|
+
You can enable extensive logging. This is useful to see whether capybara-lockstep has an effect on your tests, or to debug why synchronization is taking too long.
|
196
|
+
|
197
|
+
To enable the log, say this before or during a test:
|
192
198
|
|
193
199
|
```ruby
|
194
200
|
Capybara::Lockstep.debug = true
|
195
201
|
```
|
196
202
|
|
197
|
-
You should now see messages like this
|
203
|
+
You should now see messages like this on your standard output:
|
204
|
+
|
205
|
+
```
|
206
|
+
[capybara-lockstep] Synchronizing
|
207
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
208
|
+
[capybara-lockstep] Synchronized successfully
|
209
|
+
```
|
210
|
+
|
211
|
+
You should also see messages like this in your browser's JavaScript console:
|
198
212
|
|
199
213
|
```
|
200
|
-
[
|
214
|
+
[capybara-lockstep] Started work: fetch /path [3 jobs]
|
215
|
+
[capybara-lockstep] Finished work: fetch /path [2 jobs]
|
201
216
|
```
|
202
217
|
|
218
|
+
|
219
|
+
### Using a logger
|
220
|
+
|
203
221
|
You may also configure logging to an existing logger object:
|
204
222
|
|
205
223
|
```ruby
|
@@ -209,7 +227,9 @@ Capybara::Lockstep.debug = Rails.logger
|
|
209
227
|
|
210
228
|
## Disabling synchronization
|
211
229
|
|
212
|
-
|
230
|
+
Sometimes you want to disable browser synchronization, e.g. to observe a loading spinner during a long-running request.
|
231
|
+
|
232
|
+
To disable synchronization:
|
213
233
|
|
214
234
|
```ruby
|
215
235
|
begin
|
@@ -220,10 +240,11 @@ ensure
|
|
220
240
|
end
|
221
241
|
```
|
222
242
|
|
243
|
+
## Synchronization timeout
|
223
244
|
|
224
|
-
|
245
|
+
By default capybara-lockstep will wait `Capybara.default_max_wait_time` seconds for the page initialize and for JavaScript and AJAX request to finish.
|
225
246
|
|
226
|
-
|
247
|
+
When synchronization times out, capybara-lockstep will log but not raise an error.
|
227
248
|
|
228
249
|
You can configure a different timeout:
|
229
250
|
|
@@ -231,94 +252,77 @@ You can configure a different timeout:
|
|
231
252
|
Capybara::Lockstep.timeout = 5 # seconds
|
232
253
|
```
|
233
254
|
|
255
|
+
To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
|
234
256
|
|
257
|
+
```ruby
|
258
|
+
Capybara::Lockstep.timeout = nil
|
259
|
+
```
|
235
260
|
|
236
261
|
|
237
|
-
##
|
262
|
+
## Manual synchronization
|
238
263
|
|
239
|
-
capybara-lockstep
|
264
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
|
240
265
|
|
241
|
-
For additional edge cases you may
|
266
|
+
For additional edge cases you may manually tell capybara-lockstep to wait. The following Ruby method will block until the browser is idle:
|
242
267
|
|
243
|
-
|
268
|
+
```ruby
|
269
|
+
Capybara::Lockstep.synchronize
|
270
|
+
```
|
271
|
+
|
272
|
+
You may also synchronize from your client-side JavaScript. The following will run the given callback once the browser is idle:
|
244
273
|
|
245
274
|
```js
|
246
|
-
|
247
|
-
CapybaraLockstep.startWork()
|
248
|
-
}
|
275
|
+
CapybaraLockstep.synchronize(callback)
|
249
276
|
```
|
250
277
|
|
251
|
-
|
278
|
+
## Signaling asynchronous work
|
252
279
|
|
253
280
|
If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
|
254
281
|
|
255
282
|
```js
|
256
|
-
CapybaraLockstep.startWork()
|
283
|
+
CapybaraLockstep.startWork('Eject warp core')
|
257
284
|
doAsynchronousWork().then(function() {
|
258
|
-
CapybaraLockstep.stopWork()
|
285
|
+
CapybaraLockstep.stopWork('Eject warp core')
|
259
286
|
})
|
260
287
|
```
|
261
288
|
|
262
|
-
|
263
|
-
|
264
|
-
You can query capybara-lockstep whether it considers the browser to be busy or idle:
|
289
|
+
The string argument is used for logging (when logging is enabled). In this case you should see messages like this in your browser's JavaScript console:
|
265
290
|
|
266
|
-
```
|
267
|
-
|
268
|
-
|
291
|
+
```text
|
292
|
+
[capybara-lockstep] Started work: Eject warp core [1 jobs]
|
293
|
+
[capybara-lockstep] Finished work: Eject warp core [0 jobs]
|
269
294
|
```
|
270
295
|
|
271
|
-
|
296
|
+
You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
|
272
297
|
|
273
|
-
```js
|
274
|
-
CapybaraLockstep.awaitIdle(callback)
|
275
|
-
```
|
276
298
|
|
277
|
-
##
|
299
|
+
## Note on interacting with the JavaScript API
|
278
300
|
|
279
|
-
capybara-lockstep
|
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:
|
301
|
+
If you only load capybara-lockstep in tests you, should check for the `CapybaraLockstep` global to be defined before you interact with the JavaScript API.
|
287
302
|
|
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
303
|
```
|
297
|
-
|
298
|
-
|
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
|
304
|
+
if (window.CapybaraLockstep) {
|
305
|
+
// interact with CapybaraLockstep
|
306
|
+
}
|
305
307
|
```
|
306
308
|
|
307
|
-
|
308
309
|
## Development
|
309
310
|
|
310
311
|
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
|
|
312
313
|
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
|
|
315
|
+
|
314
316
|
## Contributing
|
315
317
|
|
316
318
|
Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
|
317
319
|
|
320
|
+
|
318
321
|
## License
|
319
322
|
|
320
323
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
321
324
|
|
325
|
+
|
322
326
|
## Credits
|
323
327
|
|
324
328
|
Henning Koch ([@triskweline](https://twitter.com/triskweline)) from [makandra](https://makandra.com).
|
data/lib/capybara-lockstep.rb
CHANGED
@@ -10,8 +10,8 @@ end
|
|
10
10
|
|
11
11
|
require_relative 'capybara-lockstep/version'
|
12
12
|
require_relative 'capybara-lockstep/errors'
|
13
|
-
require_relative 'capybara-lockstep/patiently'
|
14
13
|
require_relative 'capybara-lockstep/configuration'
|
14
|
+
require_relative 'capybara-lockstep/logging'
|
15
15
|
require_relative 'capybara-lockstep/lockstep'
|
16
16
|
require_relative 'capybara-lockstep/capybara_ext'
|
17
17
|
require_relative 'capybara-lockstep/helper'
|
@@ -2,18 +2,23 @@ module Capybara
|
|
2
2
|
module Lockstep
|
3
3
|
module VisitWithWaiting
|
4
4
|
def visit(*args, &block)
|
5
|
-
|
5
|
+
url = args[0]
|
6
|
+
# Some of our apps have a Cucumber step that changes drivers mid-scenario.
|
7
|
+
# It works by creating a new Capybara session and re-visits the URL from the
|
8
|
+
# previous session. If this happens before a URL is ever loaded,
|
9
|
+
# it re-visits the URL "data:", which will never "finish" initializing.
|
10
|
+
# Also when opening a new tab via Capybara, the initial URL is about:blank.
|
11
|
+
visiting_remote_url = !(url.start_with?('data:') || url.start_with?('about:'))
|
6
12
|
|
7
|
-
|
13
|
+
if visiting_remote_url
|
14
|
+
# We're about to leave this screen, killing all in-flight requests.
|
15
|
+
Capybara::Lockstep.synchronize
|
16
|
+
end
|
8
17
|
|
9
18
|
super(*args, &block).tap do
|
10
|
-
# There is a step that changes drivers mid-scenario.
|
11
|
-
# It works by creating a new Capybara session and re-visits the
|
12
|
-
# URL from the previous session. If this happens before a URL is ever
|
13
|
-
# loaded, it re-visits the URL "data:", which will never "finish"
|
14
|
-
# initializing.
|
15
19
|
if visiting_remote_url
|
16
|
-
|
20
|
+
# puts "After visit: unsynchronizing"
|
21
|
+
Capybara::Lockstep.synchronized = false
|
17
22
|
end
|
18
23
|
end
|
19
24
|
end
|
@@ -28,13 +33,12 @@ end
|
|
28
33
|
|
29
34
|
module Capybara
|
30
35
|
module Lockstep
|
31
|
-
module
|
32
|
-
def
|
36
|
+
module UnsychronizeAfter
|
37
|
+
def unsychronize_after(meth)
|
33
38
|
mod = Module.new do
|
34
39
|
define_method meth do |*args, &block|
|
35
|
-
Capybara::Lockstep.catch_up
|
36
40
|
super(*args, &block).tap do
|
37
|
-
Capybara::Lockstep.
|
41
|
+
Capybara::Lockstep.synchronized = false
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
@@ -62,21 +66,21 @@ end
|
|
62
66
|
|
63
67
|
node_classes.each do |node_class|
|
64
68
|
node_class.class_eval do
|
65
|
-
extend Capybara::Lockstep::
|
69
|
+
extend Capybara::Lockstep::UnsychronizeAfter
|
66
70
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
71
|
+
unsychronize_after :set
|
72
|
+
unsychronize_after :select_option
|
73
|
+
unsychronize_after :unselect_option
|
74
|
+
unsychronize_after :click
|
75
|
+
unsychronize_after :right_click
|
76
|
+
unsychronize_after :double_click
|
77
|
+
unsychronize_after :send_keys
|
78
|
+
unsychronize_after :hover
|
79
|
+
unsychronize_after :drag_to
|
80
|
+
unsychronize_after :drop
|
81
|
+
unsychronize_after :scroll_by
|
82
|
+
unsychronize_after :scroll_to
|
83
|
+
unsychronize_after :trigger
|
80
84
|
end
|
81
85
|
end
|
82
86
|
|
@@ -84,7 +88,9 @@ module Capybara
|
|
84
88
|
module Lockstep
|
85
89
|
module SynchronizeWithCatchUp
|
86
90
|
def synchronize(*args, &block)
|
87
|
-
|
91
|
+
# This method is called very frequently by capybara.
|
92
|
+
# We use the { lazy } option to only synchronize when we're out of sync.
|
93
|
+
Capybara::Lockstep.synchronize(lazy: true)
|
88
94
|
|
89
95
|
super(*args, &block)
|
90
96
|
end
|
@@ -3,7 +3,7 @@ module Capybara
|
|
3
3
|
module Configuration
|
4
4
|
|
5
5
|
def timeout
|
6
|
-
@timeout
|
6
|
+
@timeout.nil? ? Capybara.default_max_wait_time : @timeout
|
7
7
|
end
|
8
8
|
|
9
9
|
def timeout=(seconds)
|
@@ -11,11 +11,31 @@ module Capybara
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def debug?
|
14
|
-
@debug
|
14
|
+
# @debug may also be a Logger object, so convert it to a boolean
|
15
|
+
@debug.nil? ? false : !!@debug
|
15
16
|
end
|
16
17
|
|
17
18
|
def debug=(debug)
|
18
19
|
@debug = debug
|
20
|
+
if debug
|
21
|
+
target_prose = (is_logger?(debug) ? 'Ruby logger' : 'STDOUT')
|
22
|
+
log "Logging to #{target_prose} and browser console"
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
with_max_wait_time(2) do
|
27
|
+
page.execute_script(<<~JS)
|
28
|
+
if (window.CapybaraLockstep) {
|
29
|
+
CapybaraLockstep.setDebug(#{debug.to_json})
|
30
|
+
}
|
31
|
+
JS
|
32
|
+
end
|
33
|
+
rescue StandardError => e
|
34
|
+
log "#{e.class.name} while enabling logs in browser: #{e.message}"
|
35
|
+
# Don't fail. The next page load will include the snippet with debugging enabled.
|
36
|
+
end
|
37
|
+
|
38
|
+
@debug
|
19
39
|
end
|
20
40
|
|
21
41
|
def enabled?
|
@@ -30,6 +50,10 @@ module Capybara
|
|
30
50
|
@enabled = enabled
|
31
51
|
end
|
32
52
|
|
53
|
+
def disabled?
|
54
|
+
!enabled?
|
55
|
+
end
|
56
|
+
|
33
57
|
private
|
34
58
|
|
35
59
|
def javascript_driver?
|
@@ -1,6 +1,7 @@
|
|
1
1
|
window.CapybaraLockstep = (function() {
|
2
|
-
|
3
|
-
|
2
|
+
let count = 0
|
3
|
+
let idleCallbacks = []
|
4
|
+
let debug = false
|
4
5
|
|
5
6
|
function isIdle() {
|
6
7
|
// Can't check for document.readyState or body.initializing here,
|
@@ -13,31 +14,51 @@ window.CapybaraLockstep = (function() {
|
|
13
14
|
return !isIdle()
|
14
15
|
}
|
15
16
|
|
16
|
-
function
|
17
|
-
|
17
|
+
function log(...args) {
|
18
|
+
if (debug) {
|
19
|
+
args[0] = '%c[capybara-lockstep] ' + args[0]
|
20
|
+
args.splice(1, 0, 'color: #666666')
|
21
|
+
console.log.apply(console, args)
|
22
|
+
}
|
18
23
|
}
|
19
24
|
|
20
|
-
function
|
21
|
-
|
22
|
-
|
25
|
+
function logPositive(...args) {
|
26
|
+
args[0] = '%c' + args[0]
|
27
|
+
log(args[0], 'color: #117722', ...args.slice(1))
|
23
28
|
}
|
24
29
|
|
25
|
-
function
|
26
|
-
|
27
|
-
|
30
|
+
function logNegative(...args) {
|
31
|
+
args[0] = '%c' + args[0]
|
32
|
+
log(args[0], 'color: #cc3311', ...args.slice(1))
|
28
33
|
}
|
29
34
|
|
30
|
-
function
|
31
|
-
|
32
|
-
|
35
|
+
function startWork(tag) {
|
36
|
+
count++
|
37
|
+
if (tag) {
|
38
|
+
logNegative('Started work: %s [%d jobs]', tag, count)
|
39
|
+
}
|
33
40
|
}
|
34
41
|
|
35
|
-
function
|
42
|
+
function startWorkUntil(promise, tag) {
|
43
|
+
startWork(tag)
|
44
|
+
promise.then(stopWork, stopWork)
|
45
|
+
}
|
46
|
+
|
47
|
+
function startWorkForMicrotask(tag) {
|
48
|
+
startWork(tag)
|
49
|
+
Promise.resolve().then(stopWork.bind(this, tag))
|
50
|
+
}
|
51
|
+
|
52
|
+
function stopWork(tag) {
|
36
53
|
count--
|
37
54
|
|
55
|
+
if (tag) {
|
56
|
+
logPositive('Finished work: %s [%d jobs]', tag, count)
|
57
|
+
}
|
58
|
+
|
38
59
|
if (isIdle()) {
|
39
60
|
idleCallbacks.forEach(function(callback) {
|
40
|
-
callback('
|
61
|
+
callback('Finished waiting for JavaScript')
|
41
62
|
})
|
42
63
|
idleCallbacks = []
|
43
64
|
}
|
@@ -48,29 +69,36 @@ window.CapybaraLockstep = (function() {
|
|
48
69
|
return
|
49
70
|
}
|
50
71
|
|
51
|
-
|
72
|
+
let oldFetch = window.fetch
|
52
73
|
window.fetch = function() {
|
53
|
-
|
54
|
-
startWorkUntil(promise)
|
74
|
+
let promise = oldFetch.apply(this, arguments)
|
75
|
+
startWorkUntil(promise, 'fetch ' + arguments[0])
|
55
76
|
return promise
|
56
77
|
}
|
57
78
|
}
|
58
79
|
|
59
80
|
function trackXHR() {
|
60
|
-
|
81
|
+
let oldOpen = XMLHttpRequest.prototype.open
|
82
|
+
let oldSend = XMLHttpRequest.prototype.send
|
83
|
+
|
84
|
+
XMLHttpRequest.prototype.open = function() {
|
85
|
+
this.capybaraLockstepURL = arguments[1]
|
86
|
+
return oldOpen.apply(this, arguments)
|
87
|
+
}
|
61
88
|
|
62
89
|
XMLHttpRequest.prototype.send = function() {
|
63
|
-
|
90
|
+
let workTag = 'XHR to '+ this.capybaraLockstepURL
|
91
|
+
startWork(workTag)
|
64
92
|
|
65
93
|
try {
|
66
94
|
this.addEventListener('readystatechange', function(event) {
|
67
|
-
if (this.readyState === 4) { stopWork() }
|
95
|
+
if (this.readyState === 4) { stopWork(workTag) }
|
68
96
|
}.bind(this))
|
69
97
|
return oldSend.apply(this, arguments)
|
70
98
|
} catch (e) {
|
71
99
|
// If we get a sync exception during request dispatch
|
72
100
|
// we assume the request never went out.
|
73
|
-
stopWork()
|
101
|
+
stopWork(workTag)
|
74
102
|
throw e
|
75
103
|
}
|
76
104
|
}
|
@@ -89,25 +117,12 @@ window.CapybaraLockstep = (function() {
|
|
89
117
|
})
|
90
118
|
}
|
91
119
|
|
92
|
-
function onInteraction() {
|
120
|
+
function onInteraction(event) {
|
93
121
|
// We wait until the end of this microtask, assuming that any callback that
|
94
122
|
// would queue an AJAX request or load additional scripts will run by then.
|
95
123
|
startWorkForMicrotask()
|
96
124
|
}
|
97
125
|
|
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
126
|
function trackDynamicScripts() {
|
112
127
|
if (!window.MutationObserver) {
|
113
128
|
return
|
@@ -116,7 +131,7 @@ window.CapybaraLockstep = (function() {
|
|
116
131
|
// Dynamic imports or analytics snippets may insert a <script src>
|
117
132
|
// tag that loads and executes additional JavaScript. We want to be isBusy()
|
118
133
|
// until such scripts have loaded or errored.
|
119
|
-
|
134
|
+
let observer = new MutationObserver(onAnyElementChanged)
|
120
135
|
observer.observe(document, { subtree: true, childList: true })
|
121
136
|
}
|
122
137
|
|
@@ -130,32 +145,56 @@ window.CapybaraLockstep = (function() {
|
|
130
145
|
// Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
|
131
146
|
// not resolve in the next microtask but in the next *task* (it makes itself
|
132
147
|
// async using setTimoeut()). Hence we need to wait for it in addition to XHR.
|
133
|
-
|
134
|
-
jQuery.ajax = function
|
135
|
-
|
148
|
+
let oldAjax = window.jQuery.ajax
|
149
|
+
window.jQuery.ajax = function() {
|
150
|
+
let promise = oldAjax.apply(this, arguments)
|
136
151
|
startWorkUntil(promise)
|
137
152
|
return promise
|
138
153
|
}
|
139
154
|
})
|
140
155
|
}
|
141
156
|
|
157
|
+
let INITIALIZING_ATTRIBUTE = 'data-initializing'
|
158
|
+
|
159
|
+
function trackHydration() {
|
160
|
+
// Until we have a body on which we can observe [data-initializing]
|
161
|
+
// we consider ourselves busy.
|
162
|
+
startWork()
|
163
|
+
whenReady(function() {
|
164
|
+
stopWork()
|
165
|
+
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
166
|
+
startWork('Page initialization')
|
167
|
+
let observer = new MutationObserver(onInitializingAttributeChanged)
|
168
|
+
observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
|
169
|
+
}
|
170
|
+
})
|
171
|
+
}
|
172
|
+
|
173
|
+
function onInitializingAttributeChanged() {
|
174
|
+
if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
175
|
+
stopWork('Page initialization')
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
142
179
|
function isRemoteScript(node) {
|
143
180
|
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
|
144
|
-
|
145
|
-
|
181
|
+
let src = node.getAttribute('src')
|
182
|
+
let type = node.getAttribute('type')
|
146
183
|
|
147
184
|
return (src && (!type || /javascript/i.test(type)))
|
148
185
|
}
|
149
186
|
}
|
150
187
|
|
151
188
|
function onRemoteScriptAdded(script) {
|
152
|
-
|
189
|
+
let workTag = 'Remote script ' + script.getAttribute('src')
|
190
|
+
startWork(workTag)
|
191
|
+
let taggedStopWork = stopWork.bind(this, workTag)
|
153
192
|
// Chrome runs a remote <script> *before* the load event fires.
|
154
|
-
script.addEventListener('load',
|
155
|
-
script.addEventListener('error',
|
193
|
+
script.addEventListener('load', taggedStopWork)
|
194
|
+
script.addEventListener('error', taggedStopWork)
|
156
195
|
}
|
157
196
|
|
158
|
-
function
|
197
|
+
function onAnyElementChanged(changes) {
|
159
198
|
changes.forEach(function(change) {
|
160
199
|
change.addedNodes.forEach(function(addedNode) {
|
161
200
|
if (isRemoteScript(addedNode)) {
|
@@ -168,7 +207,7 @@ window.CapybaraLockstep = (function() {
|
|
168
207
|
function whenReady(callback) {
|
169
208
|
// Values are "loading", "interactive" and "completed".
|
170
209
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
|
171
|
-
if (document.readyState
|
210
|
+
if (document.readyState !== 'loading') {
|
172
211
|
callback()
|
173
212
|
} else {
|
174
213
|
document.addEventListener('DOMContentLoaded', callback)
|
@@ -179,12 +218,12 @@ window.CapybaraLockstep = (function() {
|
|
179
218
|
trackFetch()
|
180
219
|
trackXHR()
|
181
220
|
trackInteraction()
|
182
|
-
trackHistory()
|
183
221
|
trackDynamicScripts()
|
184
222
|
trackJQuery()
|
223
|
+
trackHydration()
|
185
224
|
}
|
186
225
|
|
187
|
-
function
|
226
|
+
function synchronize(callback) {
|
188
227
|
if (isIdle()) {
|
189
228
|
callback()
|
190
229
|
} else {
|
@@ -196,9 +235,10 @@ window.CapybaraLockstep = (function() {
|
|
196
235
|
track: track,
|
197
236
|
startWork: startWork,
|
198
237
|
stopWork: stopWork,
|
199
|
-
|
200
|
-
|
201
|
-
|
238
|
+
synchronize: synchronize,
|
239
|
+
set debug(newDebug) {
|
240
|
+
debug = newDebug
|
241
|
+
}
|
202
242
|
}
|
203
243
|
})()
|
204
244
|
|
@@ -17,7 +17,13 @@ module Capybara
|
|
17
17
|
tag_options[:nonce] = options.fetch(:nonce, true)
|
18
18
|
end
|
19
19
|
|
20
|
-
|
20
|
+
full_js = capybara_lockstep_js
|
21
|
+
|
22
|
+
if (debug = options.fetch(:debug, Lockstep.debug?))
|
23
|
+
full_js += "\nCapybaraLockstep.debug = #{debug.to_json}"
|
24
|
+
end
|
25
|
+
|
26
|
+
javascript_tag(full_js, tag_options)
|
21
27
|
end
|
22
28
|
|
23
29
|
end
|
@@ -1,131 +1,105 @@
|
|
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 before initial Capybara visit'
|
5
|
+
ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
|
6
|
+
ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
|
7
|
+
|
3
8
|
class << self
|
4
|
-
include Patiently
|
5
9
|
include Configuration
|
10
|
+
include Logging
|
6
11
|
|
7
|
-
|
8
|
-
|
9
|
-
return unless enabled?
|
10
|
-
|
11
|
-
with_max_wait_time(timeout) do
|
12
|
-
message_from_js = evaluate_async_script(<<~JS)
|
13
|
-
let done = arguments[0]
|
14
|
-
if (window.CapybaraLockstep) {
|
15
|
-
CapybaraLockstep.awaitIdle(done)
|
16
|
-
} else {
|
17
|
-
done('Cannot synchronize: Capybara::Lockstep was not included in page')
|
18
|
-
}
|
19
|
-
JS
|
20
|
-
log(message_from_js)
|
21
|
-
end
|
22
|
-
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
23
|
-
log 'Cannot synchronize: Alert is open'
|
24
|
-
@delay_await_idle = true
|
25
|
-
end
|
26
|
-
|
27
|
-
def await_initialized
|
28
|
-
@delay_await_initialized = false
|
29
|
-
@delay_await_idle = false # since we're also waiting for idle
|
30
|
-
return unless enabled?
|
31
|
-
|
32
|
-
# We're retrying the initialize check every few ms.
|
33
|
-
# Don't clutter the log with dozens of identical messages.
|
34
|
-
last_logged_reason = nil
|
35
|
-
|
36
|
-
patiently(timeout) do
|
37
|
-
if (reason = initialize_reason)
|
38
|
-
if reason != last_logged_reason
|
39
|
-
log(reason)
|
40
|
-
last_logged_reason = reason
|
41
|
-
end
|
12
|
+
attr_accessor :synchronizing
|
13
|
+
alias synchronizing? synchronizing
|
42
14
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
log 'Cannot synchronize: Alert is open'
|
49
|
-
@delay_await_initialized = true
|
15
|
+
def synchronized?
|
16
|
+
value = page.instance_variable_get(:@lockstep_synchronized)
|
17
|
+
# We consider a new Capybara session to be synchronized.
|
18
|
+
# This will be set to false after our first visit().
|
19
|
+
value.nil? ? true : value
|
50
20
|
end
|
51
21
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
begin
|
56
|
-
@catching_up = true
|
57
|
-
if @delay_await_initialized
|
58
|
-
log 'Retrying synchronization'
|
59
|
-
await_initialized
|
60
|
-
# elsif browser_made_full_page_load?
|
61
|
-
# log 'Browser loaded new page'
|
62
|
-
# await_initialized
|
63
|
-
elsif @delay_await_idle
|
64
|
-
log 'Retrying synchronization'
|
65
|
-
await_idle
|
66
|
-
end
|
67
|
-
ensure
|
68
|
-
@catching_up = false
|
69
|
-
end
|
22
|
+
def synchronized=(value)
|
23
|
+
page.instance_variable_set(:@lockstep_synchronized, value)
|
70
24
|
end
|
71
25
|
|
72
|
-
def
|
73
|
-
|
74
|
-
return
|
26
|
+
def synchronize(lazy: false)
|
27
|
+
if (lazy && synchronized?) || synchronizing? || disabled?
|
28
|
+
return
|
75
29
|
end
|
76
30
|
|
77
|
-
|
78
|
-
if (window.CapybaraLockstep) {
|
79
|
-
return CapybaraLockstep.isIdle()
|
80
|
-
} else {
|
81
|
-
return 'Cannot check busy state: Capybara::Lockstep was not included in page'
|
82
|
-
}
|
83
|
-
JS
|
84
|
-
|
85
|
-
if result.is_a?(String)
|
86
|
-
log(result)
|
87
|
-
# When the snippet is missing we assume that the browser is idle.
|
88
|
-
# Otherwise we would wait forever.
|
89
|
-
true
|
90
|
-
else
|
91
|
-
result
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def busy?
|
96
|
-
!idle?
|
31
|
+
synchronize_now
|
97
32
|
end
|
98
33
|
|
99
34
|
private
|
100
35
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
36
|
+
def synchronize_now
|
37
|
+
self.synchronizing = true
|
38
|
+
self.synchronized = false
|
39
|
+
|
40
|
+
log 'Synchronizing'
|
41
|
+
|
42
|
+
begin
|
43
|
+
with_max_wait_time(timeout) do
|
44
|
+
message_from_js = evaluate_async_script(<<~JS)
|
45
|
+
let done = arguments[0]
|
46
|
+
let synchronize = () => {
|
47
|
+
if (window.CapybaraLockstep) {
|
48
|
+
CapybaraLockstep.synchronize(done)
|
49
|
+
} else {
|
50
|
+
done(#{ERROR_SNIPPET_MISSING.to_json})
|
51
|
+
}
|
52
|
+
}
|
53
|
+
let protocol = location.protocol
|
54
|
+
if (protocol === 'data:' || protocol == 'about:') {
|
55
|
+
done(#{ERROR_PAGE_MISSING.to_json})
|
56
|
+
} else if (document.readyState === 'complete') {
|
57
|
+
synchronize()
|
58
|
+
} else {
|
59
|
+
window.addEventListener('load', synchronize)
|
60
|
+
}
|
61
|
+
JS
|
62
|
+
|
63
|
+
case message_from_js
|
64
|
+
when ERROR_PAGE_MISSING
|
65
|
+
log(message_from_js)
|
66
|
+
when ERROR_SNIPPET_MISSING
|
67
|
+
log(message_from_js)
|
68
|
+
else
|
69
|
+
log message_from_js
|
70
|
+
log "Synchronized successfully"
|
71
|
+
self.synchronized = true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
|
75
|
+
log "Could not synchronize within #{timeout} seconds"
|
76
|
+
# Don't raise an error, this may happen if the server is slow to respond.
|
77
|
+
# We will retry on the next Capybara synchronize call.
|
78
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
79
|
+
log ERROR_ALERT_OPEN
|
80
|
+
# Don't raise an error, this will happen in an innocent test.
|
81
|
+
# We will retry on the next Capybara synchronize call.
|
82
|
+
rescue ::Selenium::WebDriver::Error::JavascriptError => e
|
83
|
+
# When the URL changes while a script is running, my current selenium-webdriver
|
84
|
+
# raises a Selenium::WebDriver::Error::JavascriptError with the message:
|
85
|
+
# "javascript error: document unloaded while waiting for result".
|
86
|
+
# We will retry on the next Capybara synchronize call, by then we should see
|
87
|
+
# the new page.
|
88
|
+
if e.message.include?('unload')
|
89
|
+
log ERROR_NAVIGATED_AWAY
|
90
|
+
else
|
91
|
+
unhandled_synchronize_error(e)
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
unhandled_synchronize_error(e)
|
95
|
+
ensure
|
96
|
+
self.synchronizing = false
|
97
|
+
end
|
104
98
|
end
|
105
99
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
return 'Requesting initial page'
|
110
|
-
}
|
111
|
-
|
112
|
-
if (document.readyState !== "complete") {
|
113
|
-
return 'Document is loading'
|
114
|
-
}
|
115
|
-
|
116
|
-
// The application layouts render a <body data-initializing>.
|
117
|
-
// The [data-initializing] attribute is removed by an Angular directive or Unpoly compiler (frontend).
|
118
|
-
// to signal that all elements have been activated.
|
119
|
-
if (document.querySelector('body[data-initializing]')) {
|
120
|
-
return 'DOM is being hydrated'
|
121
|
-
}
|
122
|
-
|
123
|
-
if (window.CapybaraLockstep && CapybaraLockstep.isBusy()) {
|
124
|
-
return 'JavaScript or AJAX requests are running'
|
125
|
-
}
|
126
|
-
|
127
|
-
return false
|
128
|
-
JS
|
100
|
+
def unhandled_synchronize_error(e)
|
101
|
+
log "#{e.class.name} while synchronizing: #{e.message}"
|
102
|
+
raise e
|
129
103
|
end
|
130
104
|
|
131
105
|
def page
|
@@ -144,17 +118,10 @@ module Capybara
|
|
144
118
|
end
|
145
119
|
end
|
146
120
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
# If someone set Capybara::Lockstep to a logger, use that
|
152
|
-
@debug.debug(message)
|
153
|
-
else
|
154
|
-
# Otherwise print to STDOUT
|
155
|
-
puts message
|
156
|
-
end
|
157
|
-
end
|
121
|
+
def ignoring_alerts(&block)
|
122
|
+
block.call
|
123
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
124
|
+
# no-op
|
158
125
|
end
|
159
126
|
|
160
127
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Lockstep
|
3
|
+
module Logging
|
4
|
+
def log(message)
|
5
|
+
if debug? && message.present?
|
6
|
+
message = "[capybara-lockstep] #{message}"
|
7
|
+
if is_logger?(@debug)
|
8
|
+
# If someone set Capybara::Lockstep to a logger, use that
|
9
|
+
@debug.debug(message)
|
10
|
+
else
|
11
|
+
# Otherwise print to STDOUT
|
12
|
+
puts message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def is_logger?(object)
|
20
|
+
@debug.respond_to?(:debug)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
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: 0.
|
4
|
+
version: 0.4.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: 2021-03-
|
11
|
+
date: 2021-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -77,7 +77,7 @@ files:
|
|
77
77
|
- lib/capybara-lockstep/helper.js
|
78
78
|
- lib/capybara-lockstep/helper.rb
|
79
79
|
- lib/capybara-lockstep/lockstep.rb
|
80
|
-
- lib/capybara-lockstep/
|
80
|
+
- lib/capybara-lockstep/logging.rb
|
81
81
|
- lib/capybara-lockstep/version.rb
|
82
82
|
homepage: https://github.com/makandra/capybara-lockstep
|
83
83
|
licenses:
|
@@ -1,58 +0,0 @@
|
|
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
|