capybara-lockstep 0.3.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.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
|