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 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