capybara-lockstep 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +46 -41
- data/lib/capybara-lockstep/configuration.rb +22 -2
- data/lib/capybara-lockstep/helper.js +68 -37
- data/lib/capybara-lockstep/helper.rb +7 -1
- data/lib/capybara-lockstep/lockstep.rb +4 -0
- data/lib/capybara-lockstep/logging.rb +7 -1
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +1 -1
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.lock
CHANGED
data/README.md
CHANGED
@@ -192,13 +192,15 @@ In casual testing I experienced a performance impact between +/- 10%.
|
|
192
192
|
|
193
193
|
## Debugging log
|
194
194
|
|
195
|
-
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:
|
196
198
|
|
197
199
|
```ruby
|
198
200
|
Capybara::Lockstep.debug = true
|
199
201
|
```
|
200
202
|
|
201
|
-
You should now see messages like this
|
203
|
+
You should now see messages like this on your standard output:
|
202
204
|
|
203
205
|
```
|
204
206
|
[capybara-lockstep] Synchronizing
|
@@ -206,6 +208,16 @@ You should now see messages like this during your test runs:
|
|
206
208
|
[capybara-lockstep] Synchronized successfully
|
207
209
|
```
|
208
210
|
|
211
|
+
You should also see messages like this in your browser's JavaScript console:
|
212
|
+
|
213
|
+
```
|
214
|
+
[capybara-lockstep] Started work: fetch /path [3 jobs]
|
215
|
+
[capybara-lockstep] Finished work: fetch /path [2 jobs]
|
216
|
+
```
|
217
|
+
|
218
|
+
|
219
|
+
### Using a logger
|
220
|
+
|
209
221
|
You may also configure logging to an existing logger object:
|
210
222
|
|
211
223
|
```ruby
|
@@ -215,7 +227,9 @@ Capybara::Lockstep.debug = Rails.logger
|
|
215
227
|
|
216
228
|
## Disabling synchronization
|
217
229
|
|
218
|
-
|
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:
|
219
233
|
|
220
234
|
```ruby
|
221
235
|
begin
|
@@ -226,9 +240,11 @@ ensure
|
|
226
240
|
end
|
227
241
|
```
|
228
242
|
|
229
|
-
##
|
243
|
+
## Synchronization timeout
|
244
|
+
|
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.
|
230
246
|
|
231
|
-
|
247
|
+
When synchronization times out, capybara-lockstep will log but not raise an error.
|
232
248
|
|
233
249
|
You can configure a different timeout:
|
234
250
|
|
@@ -236,69 +252,58 @@ You can configure a different timeout:
|
|
236
252
|
Capybara::Lockstep.timeout = 5 # seconds
|
237
253
|
```
|
238
254
|
|
239
|
-
|
255
|
+
To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
|
240
256
|
|
241
|
-
|
257
|
+
```ruby
|
258
|
+
Capybara::Lockstep.timeout = nil
|
259
|
+
```
|
242
260
|
|
243
|
-
For additional edge cases you may interact with capybara-lockstep from your Ruby code.
|
244
261
|
|
262
|
+
## Manual synchronization
|
245
263
|
|
246
|
-
|
264
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
|
247
265
|
|
248
|
-
|
266
|
+
For additional edge cases you may manually tell capybara-lockstep to wait. The following Ruby method will block until the browser is idle:
|
249
267
|
|
250
268
|
```ruby
|
251
269
|
Capybara::Lockstep.synchronize
|
252
270
|
```
|
253
271
|
|
254
|
-
|
255
|
-
|
256
|
-
```gherkin
|
257
|
-
When 'I wait for the page to load' do
|
258
|
-
Capybara::Lockstep.synchronize
|
259
|
-
end
|
260
|
-
```
|
261
|
-
|
262
|
-
## JavaScript API
|
263
|
-
|
264
|
-
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**.
|
265
|
-
|
266
|
-
For additional edge cases you may interact with capybara-lockstep from your own JavaScripts.
|
267
|
-
|
268
|
-
Note that when you only load the JavaScript snippet in tests you need check before calling any API functions:
|
272
|
+
You may also synchronize from your client-side JavaScript. The following will run the given callback once the browser is idle:
|
269
273
|
|
270
274
|
```js
|
271
|
-
|
272
|
-
CapybaraLockstep.startWork()
|
273
|
-
}
|
275
|
+
CapybaraLockstep.synchronize(callback)
|
274
276
|
```
|
275
277
|
|
276
|
-
|
278
|
+
## Signaling asynchronous work
|
277
279
|
|
278
280
|
If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
|
279
281
|
|
280
282
|
```js
|
281
|
-
CapybaraLockstep.startWork()
|
283
|
+
CapybaraLockstep.startWork('Eject warp core')
|
282
284
|
doAsynchronousWork().then(function() {
|
283
|
-
CapybaraLockstep.stopWork()
|
285
|
+
CapybaraLockstep.stopWork('Eject warp core')
|
284
286
|
})
|
285
287
|
```
|
286
288
|
|
287
|
-
|
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:
|
288
290
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
CapybaraLockstep.isBusy() // => false
|
293
|
-
CapybaraLockstep.isIdle() // => true
|
291
|
+
```text
|
292
|
+
[capybara-lockstep] Started work: Eject warp core [1 jobs]
|
293
|
+
[capybara-lockstep] Finished work: Eject warp core [0 jobs]
|
294
294
|
```
|
295
295
|
|
296
|
-
|
296
|
+
You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
|
297
297
|
|
298
|
-
This will run the given callback once the browser is considered to be idle:
|
299
298
|
|
300
|
-
|
301
|
-
|
299
|
+
## Note on interacting with the JavaScript API
|
300
|
+
|
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.
|
302
|
+
|
303
|
+
```
|
304
|
+
if (window.CapybaraLockstep) {
|
305
|
+
// interact with CapybaraLockstep
|
306
|
+
}
|
302
307
|
```
|
303
308
|
|
304
309
|
## Development
|
@@ -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?
|
@@ -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,28 +14,48 @@ 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
|
+
}
|
40
|
+
}
|
41
|
+
|
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))
|
33
50
|
}
|
34
51
|
|
35
|
-
function stopWork() {
|
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
61
|
callback('Finished waiting for JavaScript')
|
@@ -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,7 +117,7 @@ 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()
|
@@ -103,7 +131,7 @@ window.CapybaraLockstep = (function() {
|
|
103
131
|
// Dynamic imports or analytics snippets may insert a <script src>
|
104
132
|
// tag that loads and executes additional JavaScript. We want to be isBusy()
|
105
133
|
// until such scripts have loaded or errored.
|
106
|
-
|
134
|
+
let observer = new MutationObserver(onAnyElementChanged)
|
107
135
|
observer.observe(document, { subtree: true, childList: true })
|
108
136
|
}
|
109
137
|
|
@@ -117,16 +145,16 @@ window.CapybaraLockstep = (function() {
|
|
117
145
|
// Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
|
118
146
|
// not resolve in the next microtask but in the next *task* (it makes itself
|
119
147
|
// async using setTimoeut()). Hence we need to wait for it in addition to XHR.
|
120
|
-
|
121
|
-
jQuery.ajax = function
|
122
|
-
|
148
|
+
let oldAjax = window.jQuery.ajax
|
149
|
+
window.jQuery.ajax = function() {
|
150
|
+
let promise = oldAjax.apply(this, arguments)
|
123
151
|
startWorkUntil(promise)
|
124
152
|
return promise
|
125
153
|
}
|
126
154
|
})
|
127
155
|
}
|
128
156
|
|
129
|
-
|
157
|
+
let INITIALIZING_ATTRIBUTE = 'data-initializing'
|
130
158
|
|
131
159
|
function trackHydration() {
|
132
160
|
// Until we have a body on which we can observe [data-initializing]
|
@@ -135,8 +163,8 @@ window.CapybaraLockstep = (function() {
|
|
135
163
|
whenReady(function() {
|
136
164
|
stopWork()
|
137
165
|
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
138
|
-
startWork()
|
139
|
-
|
166
|
+
startWork('Page initialization')
|
167
|
+
let observer = new MutationObserver(onInitializingAttributeChanged)
|
140
168
|
observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
|
141
169
|
}
|
142
170
|
})
|
@@ -144,24 +172,26 @@ window.CapybaraLockstep = (function() {
|
|
144
172
|
|
145
173
|
function onInitializingAttributeChanged() {
|
146
174
|
if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
147
|
-
stopWork()
|
175
|
+
stopWork('Page initialization')
|
148
176
|
}
|
149
177
|
}
|
150
178
|
|
151
179
|
function isRemoteScript(node) {
|
152
180
|
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
|
153
|
-
|
154
|
-
|
181
|
+
let src = node.getAttribute('src')
|
182
|
+
let type = node.getAttribute('type')
|
155
183
|
|
156
184
|
return (src && (!type || /javascript/i.test(type)))
|
157
185
|
}
|
158
186
|
}
|
159
187
|
|
160
188
|
function onRemoteScriptAdded(script) {
|
161
|
-
|
189
|
+
let workTag = 'Remote script ' + script.getAttribute('src')
|
190
|
+
startWork(workTag)
|
191
|
+
let taggedStopWork = stopWork.bind(this, workTag)
|
162
192
|
// Chrome runs a remote <script> *before* the load event fires.
|
163
|
-
script.addEventListener('load',
|
164
|
-
script.addEventListener('error',
|
193
|
+
script.addEventListener('load', taggedStopWork)
|
194
|
+
script.addEventListener('error', taggedStopWork)
|
165
195
|
}
|
166
196
|
|
167
197
|
function onAnyElementChanged(changes) {
|
@@ -206,8 +236,9 @@ window.CapybaraLockstep = (function() {
|
|
206
236
|
startWork: startWork,
|
207
237
|
stopWork: stopWork,
|
208
238
|
synchronize: synchronize,
|
209
|
-
|
210
|
-
|
239
|
+
set debug(newDebug) {
|
240
|
+
debug = newDebug
|
241
|
+
}
|
211
242
|
}
|
212
243
|
})()
|
213
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
|
@@ -71,6 +71,10 @@ module Capybara
|
|
71
71
|
self.synchronized = true
|
72
72
|
end
|
73
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.
|
74
78
|
rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
75
79
|
log ERROR_ALERT_OPEN
|
76
80
|
# Don't raise an error, this will happen in an innocent test.
|
@@ -4,7 +4,7 @@ module Capybara
|
|
4
4
|
def log(message)
|
5
5
|
if debug? && message.present?
|
6
6
|
message = "[capybara-lockstep] #{message}"
|
7
|
-
if
|
7
|
+
if is_logger?(@debug)
|
8
8
|
# If someone set Capybara::Lockstep to a logger, use that
|
9
9
|
@debug.debug(message)
|
10
10
|
else
|
@@ -13,6 +13,12 @@ module Capybara
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def is_logger?(object)
|
20
|
+
@debug.respond_to?(:debug)
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|