capybara-lockstep 0.2.2 → 0.3.3
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 +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
|