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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86fa6ca6223224f8ac4b53c973cf961287964d11b129d8aa89b7b4dc81d4bba4
4
- data.tar.gz: f250e201b8ef467fd46d1e30654d8e2c88366c23ad8cb314e1031cb44268dcd7
3
+ metadata.gz: 783200fa3cbde0166b11c213bb677250021185b7c9898a3fd95594dd8433b89a
4
+ data.tar.gz: 77efe617f3ce7f138da78e8b9db031b9a8f75f07d3afe8379ae903266a9fa389
5
5
  SHA512:
6
- metadata.gz: 846816f82797d02dc81869e449afaf99c3fdfe89682d6584a56b3416cbb5942138a38ae457dfebcbb4fc7f20ed2673766a02287911b258445c5a9b800d613f9c
7
- data.tar.gz: 30064e290e2caed298e5868b0f1cb9c1f5aa34444895fa8cefd4d283b0433528dd1ee7793526f0652fbc11511ed3e50e9a6eb999b00f6a898411480616422199
6
+ metadata.gz: 6ec80d343f193b6b5a166bae1bf5ab6be2e7ae2e3c82d903596e4c604985d96382cce5012df5abdb714da614d8741f062386d60a47a1a3340734c22e46814949
7
+ data.tar.gz: 02e870bc8b8377be23e0ec97086219c7fd75a098f6f4eeda953dac86b7eb3a957d93984fb26265e8322c21989daa28cf4713ee3dc1772cd652f493e7999b6b57
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (0.3.3)
4
+ capybara-lockstep (0.4.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  selenium-webdriver (>= 3)
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 can print to the console whenever it waits for the browser. To enable the log:
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 during your test runs:
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
- If for some reason you want to disable browser synchronization for a while, you can do it like this:
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
- ## Timeout
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
- By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
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
- ## Ruby API
255
+ To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
240
256
 
241
- capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
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
- ### Waiting until the browser is idle
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
- This will block until the document was loaded, the DOM has been hydrated and all AJAX requests have concluded:
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
- 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
- ```
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
- if (window.CapybaraLockstep) {
272
- CapybaraLockstep.startWork()
273
- }
275
+ CapybaraLockstep.synchronize(callback)
274
276
  ```
275
277
 
276
- ### Signaling asynchronous work
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
- ### Checking if the browser is busy
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
- You can query capybara-lockstep whether it considers the browser to be busy or idle:
290
-
291
- ```js
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
- ### Waiting until the browser is idle
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
- ```js
301
- CapybaraLockstep.synchronize(callback)
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 || 10
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.nil? ? false : @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
- var count = 0
3
- var idleCallbacks = []
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 startWork() {
17
- count++
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 startWorkUntil(promise) {
21
- startWork()
22
- promise.then(stopWork, stopWork)
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 startWorkForTime(time) {
26
- startWork()
27
- setTimeout(stopWork, time)
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 startWorkForMicrotask() {
31
- startWork()
32
- Promise.resolve().then(stopWork)
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
- var oldFetch = window.fetch
72
+ let oldFetch = window.fetch
52
73
  window.fetch = function() {
53
- var promise = oldFetch.apply(this, arguments)
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
- var oldSend = XMLHttpRequest.prototype.send
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
- startWork()
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
- var observer = new MutationObserver(onAnyElementChanged)
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
- var oldAjax = jQuery.ajax
121
- jQuery.ajax = function () {
122
- var promise = oldAjax.apply(this, arguments)
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
- var INITIALIZING_ATTRIBUTE = 'data-initializing'
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
- var observer = new MutationObserver(onInitializingAttributeChanged)
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
- var src = node.getAttribute('src')
154
- var type = node.getAttribute('type')
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
- startWork()
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', stopWork)
164
- script.addEventListener('error', stopWork)
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
- isIdle: isIdle,
210
- isBusy: isBusy
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
- javascript_tag(capybara_lockstep_js, tag_options)
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 @debug.respond_to?(:debug)
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
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "0.3.3"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-lockstep
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch