capybara-lockstep 0.2.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +18 -13
- data/README.md +37 -38
- data/lib/capybara-lockstep.rb +1 -1
- data/lib/capybara-lockstep/capybara_ext.rb +62 -30
- data/lib/capybara-lockstep/configuration.rb +4 -0
- data/lib/capybara-lockstep/helper.js +29 -20
- data/lib/capybara-lockstep/lockstep.rb +84 -98
- data/lib/capybara-lockstep/logging.rb +18 -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: 86fa6ca6223224f8ac4b53c973cf961287964d11b129d8aa89b7b4dc81d4bba4
|
4
|
+
data.tar.gz: f250e201b8ef467fd46d1e30654d8e2c88366c23ad8cb314e1031cb44268dcd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 846816f82797d02dc81869e449afaf99c3fdfe89682d6584a56b3416cbb5942138a38ae457dfebcbb4fc7f20ed2673766a02287911b258445c5a9b800d613f9c
|
7
|
+
data.tar.gz: 30064e290e2caed298e5868b0f1cb9c1f5aa34444895fa8cefd4d283b0433528dd1ee7793526f0652fbc11511ed3e50e9a6eb999b00f6a898411480616422199
|
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.3.3)
|
5
5
|
activesupport (>= 3.2)
|
6
6
|
capybara (>= 2.0)
|
7
7
|
selenium-webdriver (>= 3)
|
@@ -9,13 +9,15 @@ PATH
|
|
9
9
|
GEM
|
10
10
|
remote: https://rubygems.org/
|
11
11
|
specs:
|
12
|
-
activesupport (
|
12
|
+
activesupport (6.1.3)
|
13
13
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
-
i18n (>=
|
15
|
-
minitest (
|
16
|
-
tzinfo (~>
|
14
|
+
i18n (>= 1.6, < 2)
|
15
|
+
minitest (>= 5.1)
|
16
|
+
tzinfo (~> 2.0)
|
17
|
+
zeitwerk (~> 2.3)
|
17
18
|
addressable (2.7.0)
|
18
19
|
public_suffix (>= 2.0.2, < 5.0)
|
20
|
+
byebug (11.1.3)
|
19
21
|
capybara (3.35.3)
|
20
22
|
addressable
|
21
23
|
mini_mime (>= 0.1.3)
|
@@ -25,13 +27,15 @@ GEM
|
|
25
27
|
regexp_parser (>= 1.5, < 3.0)
|
26
28
|
xpath (~> 3.2)
|
27
29
|
childprocess (3.0.0)
|
28
|
-
concurrent-ruby (1.1.
|
30
|
+
concurrent-ruby (1.1.8)
|
29
31
|
diff-lcs (1.3)
|
30
|
-
i18n (1.8.
|
32
|
+
i18n (1.8.9)
|
31
33
|
concurrent-ruby (~> 1.0)
|
32
34
|
mini_mime (1.0.2)
|
33
|
-
|
34
|
-
|
35
|
+
mini_portile2 (2.5.0)
|
36
|
+
minitest (5.14.4)
|
37
|
+
nokogiri (1.11.1)
|
38
|
+
mini_portile2 (~> 2.5.0)
|
35
39
|
racc (~> 1.4)
|
36
40
|
public_suffix (4.0.6)
|
37
41
|
racc (1.5.2)
|
@@ -53,20 +57,21 @@ GEM
|
|
53
57
|
diff-lcs (>= 1.2.0, < 2.0)
|
54
58
|
rspec-support (~> 3.7.0)
|
55
59
|
rspec-support (3.7.0)
|
56
|
-
rubyzip (
|
60
|
+
rubyzip (2.3.0)
|
57
61
|
selenium-webdriver (3.142.7)
|
58
62
|
childprocess (>= 0.5, < 4.0)
|
59
63
|
rubyzip (>= 1.2.2)
|
60
|
-
|
61
|
-
|
62
|
-
thread_safe (~> 0.1)
|
64
|
+
tzinfo (2.0.4)
|
65
|
+
concurrent-ruby (~> 1.0)
|
63
66
|
xpath (3.2.0)
|
64
67
|
nokogiri (~> 1.8)
|
68
|
+
zeitwerk (2.4.2)
|
65
69
|
|
66
70
|
PLATFORMS
|
67
71
|
ruby
|
68
72
|
|
69
73
|
DEPENDENCIES
|
74
|
+
byebug
|
70
75
|
capybara-lockstep!
|
71
76
|
rake (~> 13.0)
|
72
77
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -59,7 +59,7 @@ Installation
|
|
59
59
|
Check if your application satisfies all requirements for capybara-lockstep:
|
60
60
|
|
61
61
|
- Capybara 2 or higher.
|
62
|
-
- Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-
|
62
|
+
- Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-lockstep deactivates itself for any other driver.
|
63
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
64
|
- This gem was only tested with Rails, but there's no Rails dependency.
|
65
65
|
|
@@ -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,7 +187,7 @@ 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
|
@@ -197,7 +201,9 @@ Capybara::Lockstep.debug = true
|
|
197
201
|
You should now see messages like this during your test runs:
|
198
202
|
|
199
203
|
```
|
200
|
-
[
|
204
|
+
[capybara-lockstep] Synchronizing
|
205
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
206
|
+
[capybara-lockstep] Synchronized successfully
|
201
207
|
```
|
202
208
|
|
203
209
|
You may also configure logging to an existing logger object:
|
@@ -220,7 +226,6 @@ ensure
|
|
220
226
|
end
|
221
227
|
```
|
222
228
|
|
223
|
-
|
224
229
|
## Timeout
|
225
230
|
|
226
231
|
By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
|
@@ -231,8 +236,28 @@ You can configure a different timeout:
|
|
231
236
|
Capybara::Lockstep.timeout = 5 # seconds
|
232
237
|
```
|
233
238
|
|
239
|
+
## Ruby API
|
240
|
+
|
241
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
|
242
|
+
|
243
|
+
For additional edge cases you may interact with capybara-lockstep from your Ruby code.
|
244
|
+
|
245
|
+
|
246
|
+
### Waiting until the browser is idle
|
234
247
|
|
248
|
+
This will block until the document was loaded, the DOM has been hydrated and all AJAX requests have concluded:
|
235
249
|
|
250
|
+
```ruby
|
251
|
+
Capybara::Lockstep.synchronize
|
252
|
+
```
|
253
|
+
|
254
|
+
An example use case is a Cucumber step that explicitely waits for JavaScript to finish, in the rare occasion where capybara-lockstep hasn't picked up an event or request:
|
255
|
+
|
256
|
+
```gherkin
|
257
|
+
When 'I wait for the page to load' do
|
258
|
+
Capybara::Lockstep.synchronize
|
259
|
+
end
|
260
|
+
```
|
236
261
|
|
237
262
|
## JavaScript API
|
238
263
|
|
@@ -270,55 +295,29 @@ CapybaraLockstep.isIdle() // => true
|
|
270
295
|
|
271
296
|
### Waiting until the browser is idle
|
272
297
|
|
273
|
-
|
274
|
-
CapybaraLockstep.awaitIdle(callback)
|
275
|
-
```
|
298
|
+
This will run the given callback once the browser is considered to be idle:
|
276
299
|
|
277
|
-
|
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
|
300
|
+
```js
|
301
|
+
CapybaraLockstep.synchronize(callback)
|
305
302
|
```
|
306
303
|
|
307
|
-
|
308
304
|
## Development
|
309
305
|
|
310
306
|
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
307
|
|
312
308
|
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
309
|
|
310
|
+
|
314
311
|
## Contributing
|
315
312
|
|
316
313
|
Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
|
317
314
|
|
315
|
+
|
318
316
|
## License
|
319
317
|
|
320
318
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
321
319
|
|
320
|
+
|
322
321
|
## Credits
|
323
322
|
|
324
323
|
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'
|
@@ -1,26 +1,44 @@
|
|
1
1
|
module Capybara
|
2
2
|
module Lockstep
|
3
3
|
module VisitWithWaiting
|
4
|
-
def visit(*args,
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
def visit(*args, &block)
|
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:'))
|
12
|
+
|
13
|
+
if visiting_remote_url
|
14
|
+
# We're about to leave this screen, killing all in-flight requests.
|
15
|
+
Capybara::Lockstep.synchronize
|
16
|
+
end
|
17
|
+
|
18
|
+
super(*args, &block).tap do
|
19
|
+
if visiting_remote_url
|
20
|
+
# puts "After visit: unsynchronizing"
|
21
|
+
Capybara::Lockstep.synchronized = false
|
13
22
|
end
|
14
23
|
end
|
15
24
|
end
|
16
25
|
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
17
29
|
|
18
|
-
|
19
|
-
|
30
|
+
Capybara::Session.class_eval do
|
31
|
+
prepend Capybara::Lockstep::VisitWithWaiting
|
32
|
+
end
|
33
|
+
|
34
|
+
module Capybara
|
35
|
+
module Lockstep
|
36
|
+
module UnsychronizeAfter
|
37
|
+
def unsychronize_after(meth)
|
20
38
|
mod = Module.new do
|
21
39
|
define_method meth do |*args, &block|
|
22
40
|
super(*args, &block).tap do
|
23
|
-
Capybara::Lockstep.
|
41
|
+
Capybara::Lockstep.synchronized = false
|
24
42
|
end
|
25
43
|
end
|
26
44
|
end
|
@@ -30,10 +48,6 @@ module Capybara
|
|
30
48
|
end
|
31
49
|
end
|
32
50
|
|
33
|
-
Capybara::Session.class_eval do
|
34
|
-
prepend Capybara::Lockstep::VisitWithWaiting
|
35
|
-
end
|
36
|
-
|
37
51
|
# Capybara 3 has driver-specific Node classes which sometimes
|
38
52
|
# super to Capybara::Selenium::Node, but not always.
|
39
53
|
node_classes = [
|
@@ -52,20 +66,38 @@ end
|
|
52
66
|
|
53
67
|
node_classes.each do |node_class|
|
54
68
|
node_class.class_eval do
|
55
|
-
extend Capybara::Lockstep::
|
69
|
+
extend Capybara::Lockstep::UnsychronizeAfter
|
56
70
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
70
84
|
end
|
71
85
|
end
|
86
|
+
|
87
|
+
module Capybara
|
88
|
+
module Lockstep
|
89
|
+
module SynchronizeWithCatchUp
|
90
|
+
def synchronize(*args, &block)
|
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)
|
94
|
+
|
95
|
+
super(*args, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Capybara::Node::Base.class_eval do
|
102
|
+
prepend Capybara::Lockstep::SynchronizeWithCatchUp
|
103
|
+
end
|
@@ -37,7 +37,7 @@ window.CapybaraLockstep = (function() {
|
|
37
37
|
|
38
38
|
if (isIdle()) {
|
39
39
|
idleCallbacks.forEach(function(callback) {
|
40
|
-
callback('
|
40
|
+
callback('Finished waiting for JavaScript')
|
41
41
|
})
|
42
42
|
idleCallbacks = []
|
43
43
|
}
|
@@ -95,19 +95,6 @@ window.CapybaraLockstep = (function() {
|
|
95
95
|
startWorkForMicrotask()
|
96
96
|
}
|
97
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
98
|
function trackDynamicScripts() {
|
112
99
|
if (!window.MutationObserver) {
|
113
100
|
return
|
@@ -116,7 +103,7 @@ window.CapybaraLockstep = (function() {
|
|
116
103
|
// Dynamic imports or analytics snippets may insert a <script src>
|
117
104
|
// tag that loads and executes additional JavaScript. We want to be isBusy()
|
118
105
|
// until such scripts have loaded or errored.
|
119
|
-
var observer = new MutationObserver(
|
106
|
+
var observer = new MutationObserver(onAnyElementChanged)
|
120
107
|
observer.observe(document, { subtree: true, childList: true })
|
121
108
|
}
|
122
109
|
|
@@ -139,6 +126,28 @@ window.CapybaraLockstep = (function() {
|
|
139
126
|
})
|
140
127
|
}
|
141
128
|
|
129
|
+
var INITIALIZING_ATTRIBUTE = 'data-initializing'
|
130
|
+
|
131
|
+
function trackHydration() {
|
132
|
+
// Until we have a body on which we can observe [data-initializing]
|
133
|
+
// we consider ourselves busy.
|
134
|
+
startWork()
|
135
|
+
whenReady(function() {
|
136
|
+
stopWork()
|
137
|
+
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
138
|
+
startWork()
|
139
|
+
var observer = new MutationObserver(onInitializingAttributeChanged)
|
140
|
+
observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
|
141
|
+
}
|
142
|
+
})
|
143
|
+
}
|
144
|
+
|
145
|
+
function onInitializingAttributeChanged() {
|
146
|
+
if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
147
|
+
stopWork()
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
142
151
|
function isRemoteScript(node) {
|
143
152
|
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
|
144
153
|
var src = node.getAttribute('src')
|
@@ -155,7 +164,7 @@ window.CapybaraLockstep = (function() {
|
|
155
164
|
script.addEventListener('error', stopWork)
|
156
165
|
}
|
157
166
|
|
158
|
-
function
|
167
|
+
function onAnyElementChanged(changes) {
|
159
168
|
changes.forEach(function(change) {
|
160
169
|
change.addedNodes.forEach(function(addedNode) {
|
161
170
|
if (isRemoteScript(addedNode)) {
|
@@ -168,7 +177,7 @@ window.CapybaraLockstep = (function() {
|
|
168
177
|
function whenReady(callback) {
|
169
178
|
// Values are "loading", "interactive" and "completed".
|
170
179
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
|
171
|
-
if (document.readyState
|
180
|
+
if (document.readyState !== 'loading') {
|
172
181
|
callback()
|
173
182
|
} else {
|
174
183
|
document.addEventListener('DOMContentLoaded', callback)
|
@@ -179,12 +188,12 @@ window.CapybaraLockstep = (function() {
|
|
179
188
|
trackFetch()
|
180
189
|
trackXHR()
|
181
190
|
trackInteraction()
|
182
|
-
trackHistory()
|
183
191
|
trackDynamicScripts()
|
184
192
|
trackJQuery()
|
193
|
+
trackHydration()
|
185
194
|
}
|
186
195
|
|
187
|
-
function
|
196
|
+
function synchronize(callback) {
|
188
197
|
if (isIdle()) {
|
189
198
|
callback()
|
190
199
|
} else {
|
@@ -196,7 +205,7 @@ window.CapybaraLockstep = (function() {
|
|
196
205
|
track: track,
|
197
206
|
startWork: startWork,
|
198
207
|
stopWork: stopWork,
|
199
|
-
|
208
|
+
synchronize: synchronize,
|
200
209
|
isIdle: isIdle,
|
201
210
|
isBusy: isBusy
|
202
211
|
}
|
@@ -1,102 +1,101 @@
|
|
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
|
11
|
+
|
12
|
+
attr_accessor :synchronizing
|
13
|
+
alias synchronizing? synchronizing
|
14
|
+
|
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
|
20
|
+
end
|
21
|
+
|
22
|
+
def synchronized=(value)
|
23
|
+
page.instance_variable_set(:@lockstep_synchronized, value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def synchronize(lazy: false)
|
27
|
+
if (lazy && synchronized?) || synchronizing? || disabled?
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
synchronize_now
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
6
35
|
|
7
|
-
def
|
8
|
-
|
36
|
+
def synchronize_now
|
37
|
+
self.synchronizing = true
|
38
|
+
self.synchronized = false
|
9
39
|
|
10
|
-
|
11
|
-
|
40
|
+
log 'Synchronizing'
|
41
|
+
|
42
|
+
begin
|
12
43
|
with_max_wait_time(timeout) do
|
13
44
|
message_from_js = evaluate_async_script(<<~JS)
|
14
45
|
let done = arguments[0]
|
15
|
-
|
16
|
-
CapybaraLockstep
|
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()
|
17
58
|
} else {
|
18
|
-
|
59
|
+
window.addEventListener('load', synchronize)
|
19
60
|
}
|
20
61
|
JS
|
21
|
-
log(message_from_js)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
62
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
if reason != last_logged_reason
|
36
|
-
log(reason)
|
37
|
-
last_logged_reason = reason
|
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
|
38
72
|
end
|
39
|
-
|
40
|
-
# Raise an exception that will be retried by `patiently`
|
41
|
-
raise Busy, reason
|
42
73
|
end
|
74
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
75
|
+
log ERROR_ALERT_OPEN
|
76
|
+
# Don't raise an error, this will happen in an innocent test.
|
77
|
+
# We will retry on the next Capybara synchronize call.
|
78
|
+
rescue ::Selenium::WebDriver::Error::JavascriptError => e
|
79
|
+
# When the URL changes while a script is running, my current selenium-webdriver
|
80
|
+
# raises a Selenium::WebDriver::Error::JavascriptError with the message:
|
81
|
+
# "javascript error: document unloaded while waiting for result".
|
82
|
+
# We will retry on the next Capybara synchronize call, by then we should see
|
83
|
+
# the new page.
|
84
|
+
if e.message.include?('unload')
|
85
|
+
log ERROR_NAVIGATED_AWAY
|
86
|
+
else
|
87
|
+
unhandled_synchronize_error(e)
|
88
|
+
end
|
89
|
+
rescue StandardError => e
|
90
|
+
unhandled_synchronize_error(e)
|
91
|
+
ensure
|
92
|
+
self.synchronizing = false
|
43
93
|
end
|
44
94
|
end
|
45
95
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
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
|
96
|
+
def unhandled_synchronize_error(e)
|
97
|
+
log "#{e.class.name} while synchronizing: #{e.message}"
|
98
|
+
raise e
|
100
99
|
end
|
101
100
|
|
102
101
|
def page
|
@@ -105,12 +104,6 @@ module Capybara
|
|
105
104
|
|
106
105
|
delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
|
107
106
|
|
108
|
-
def ignoring_alerts(&block)
|
109
|
-
block.call
|
110
|
-
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
111
|
-
# noop
|
112
|
-
end
|
113
|
-
|
114
107
|
def with_max_wait_time(seconds, &block)
|
115
108
|
old_max_wait_time = Capybara.default_max_wait_time
|
116
109
|
Capybara.default_max_wait_time = seconds
|
@@ -121,17 +114,10 @@ module Capybara
|
|
121
114
|
end
|
122
115
|
end
|
123
116
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
117
|
+
def ignoring_alerts(&block)
|
118
|
+
block.call
|
119
|
+
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
120
|
+
# no-op
|
135
121
|
end
|
136
122
|
|
137
123
|
end
|
@@ -0,0 +1,18 @@
|
|
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 @debug.respond_to?(: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
|
+
end
|
17
|
+
end
|
18
|
+
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.3.3
|
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
|