capybara-lockstep 0.4.0 → 0.5.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: 783200fa3cbde0166b11c213bb677250021185b7c9898a3fd95594dd8433b89a
4
- data.tar.gz: 77efe617f3ce7f138da78e8b9db031b9a8f75f07d3afe8379ae903266a9fa389
3
+ metadata.gz: 29e90825f3d0526be3d7780f7a291eddb9094b271f53ea5dcd4331a4123cda01
4
+ data.tar.gz: 1985baa9704e28b4a0b8a38c53af53b133adc68db14b5ec1fc613cd2adc0f21c
5
5
  SHA512:
6
- metadata.gz: 6ec80d343f193b6b5a166bae1bf5ab6be2e7ae2e3c82d903596e4c604985d96382cce5012df5abdb714da614d8741f062386d60a47a1a3340734c22e46814949
7
- data.tar.gz: 02e870bc8b8377be23e0ec97086219c7fd75a098f6f4eeda953dac86b7eb3a957d93984fb26265e8322c21989daa28cf4713ee3dc1772cd652f493e7999b6b57
6
+ metadata.gz: 761be246131d0fce67dd2fb4d3ac8c7bddb0775e477ebe02b7c36139dc7e6152c0dea579cd02343b2e7f5ba3dd85ce2127fc21b323e1216584e3ebd16867a145
7
+ data.tar.gz: c0c4ad6b8bb83bf6493276bc2388446a3f6ff07b6ae8c924189851e77e9f767a551ce85ba9f04c6ebe285c0904fe3eb44ed1761f704e0ebaee1a7a638c49e26e
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (0.4.0)
4
+ capybara-lockstep (0.5.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  selenium-webdriver (>= 3)
@@ -32,10 +32,8 @@ GEM
32
32
  i18n (1.8.9)
33
33
  concurrent-ruby (~> 1.0)
34
34
  mini_mime (1.0.2)
35
- mini_portile2 (2.5.0)
36
35
  minitest (5.14.4)
37
- nokogiri (1.11.1)
38
- mini_portile2 (~> 2.5.0)
36
+ nokogiri (1.11.1-x86_64-linux)
39
37
  racc (~> 1.4)
40
38
  public_suffix (4.0.6)
41
39
  racc (1.5.2)
data/README.md CHANGED
@@ -38,17 +38,13 @@ How capybara-lockstep helps
38
38
 
39
39
  capybara-lockstep waits until the browser is idle before moving on to the next Capybara command. This greatly relieves the pressure on Capybara's retry logic.
40
40
 
41
- Whenever Capybara visits a new URL:
41
+ Whenever Capybara visits a new URL or simulates a user interaction (clicking, typing, etc.):
42
42
 
43
43
  - capybara-lockstep waits for all document resources to load.
44
44
  - capybara-lockstep waits for client-side JavaScript to render or hydrate DOM elements.
45
45
  - capybara-lockstep waits for any AJAX requests.
46
46
  - capybara-lockstep waits for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
47
-
48
- Whenever Capybara simulates a user interaction (clicking, typing, etc.):
49
-
50
- - capybara-lockstep waits for any AJAX requests.
51
- - capybara-lockstep waits for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
47
+ - capybara-lockstep waits for dynamically `<img>` or `<iframe>` elements to load.
52
48
 
53
49
 
54
50
  Installation
@@ -102,9 +98,9 @@ If you're not using Rails you can `include Capybara::Lockstep::Helper` and acces
102
98
 
103
99
  ### Signaling the end of page initialization
104
100
 
105
- Most web applications run some JavaScript after the document was loaded. This JavaScript enhances existing DOM elements ("hydration") or renders additional element into the DOM.
101
+ Most web applications run some JavaScript after a document has initially loaded. Such JavaScript usually enhances existing DOM elements ("hydration") or renders additional element into the DOM.
106
102
 
107
- capybara-lockstep needs to know when your JavaScript is done hydrating and rendering, so it can automatically wait for initialization after every Capybara `visit()`.
103
+ capybara-lockstep will synchronize more reliably if you signal when your JavaScript is done rendering the initial document. After the initial rendering, capybara-lockstep will automatically detect when the browser is busy, even if content is changed dynamically later.
108
104
 
109
105
  To signal that JavaScript is still initializing, your application layouts should render the `<body>` element with an `[data-initializing]` attribute:
110
106
 
@@ -112,10 +108,14 @@ To signal that JavaScript is still initializing, your application layouts should
112
108
  <body data-initializing>
113
109
  ```
114
110
 
115
- Your application JavaScript should remove the `[data-initializing]` attribute when it is done hydrating and rendering.
111
+ Your application JavaScript should remove the `[data-initializing]` attribute when it is done rendering the initial page.
116
112
 
117
113
  More precisely, the attribute should be removed in the same [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) ("tick") that will finish initializing. capybara-lockstep will assume that the page will be initialized by the end of this task.
118
114
 
115
+ **After the initial rendering, capybara-lockstep will automatically detect when the browser is busy, even if content is changed dynamically later. After the initial page load you no longer need to add or remove the `[data-initializing]` attribute.**
116
+
117
+ #### Example: Vanilla JS
118
+
119
119
  If all your initializing JavaScript runs synchronously on `DOMContentLoaded`, you can remove `[data-initializing]` in an event handler:
120
120
 
121
121
  ```js
@@ -125,26 +125,42 @@ document.addEventListener('DOMContentLoaded', function() {
125
125
  })
126
126
  ```
127
127
 
128
- If you do any asynchronous initialization work (like lazy-loading another script) you should only remove `[data-initializing]` once that is done:
128
+ If you call libraries during initialization, you may need to check the library code to see whether it finishes synchronously or asynchronously. Ideally a library offers a callback to notify you when it is done rendering:
129
129
 
130
130
  ```js
131
131
  document.addEventListener('DOMContentLoaded', function() {
132
- import('huge-library').then(function({ hugeLibrary }) {
133
- hugeLibrary.initialize()
134
- document.body.removeAttribute('data-initializing')
132
+ Libary.initialize({
133
+ onFinished: function() {
134
+ document.body.removeAttribute('data-initializing')
135
+ })
135
136
  })
137
+ setTimeout(function() { document.body.removeAttribute('data-initializing') })
136
138
  })
137
139
  ```
138
140
 
139
- If you call libraries during initialization, you may need to check the library code to see whether it finishes synchronously or asynchronously. E.g. if you discover that a library delays work for a task, you must also wait another task to remove `[data-initializing]`:
141
+ When a library offers no such callback, but you see in its code that the library delays work for a task, you must also wait another task to remove `[data-initializing]`:
140
142
 
141
143
  ```js
142
144
  document.addEventListener('DOMContentLoaded', function() {
143
- Libary.doWorkInNextTask()
145
+ Libary.initialize()
144
146
  setTimeout(function() { document.body.removeAttribute('data-initializing') })
145
147
  })
146
148
  ```
147
149
 
150
+ If your initialization code lazy-loads another script, you should only remove `[data-initializing]` once that is done:
151
+
152
+ ```js
153
+ document.addEventListener('DOMContentLoaded', function() {
154
+ import('huge-library').then(function({ HugeLibrary }) {
155
+ HugeLibrary.initialize()
156
+ document.body.removeAttribute('data-initializing')
157
+ })
158
+ })
159
+ ```
160
+
161
+
162
+ #### Example: Unpoly
163
+
148
164
  When you're using [Unpoly](https://unpoly.com/) initializing will usually happen synchronously in [compilers](https://unpoly.com/up.compiler). Hence a compiler is a good place to remove `[data-initializing]`:
149
165
 
150
166
  ```js
@@ -153,6 +169,8 @@ up.compiler('body', function(body) {
153
169
  })
154
170
  ```
155
171
 
172
+ #### Example: AngularJS 1
173
+
156
174
  When you're using [AngularJS 1](https://unpoly.com/) initializing will usually happen synchronously in [directives](https://docs.angularjs.org/guide/directive). Hence a directive is a good place to remove `[data-initializing]`:
157
175
 
158
176
  ```js
@@ -224,6 +242,14 @@ You may also configure logging to an existing logger object:
224
242
  Capybara::Lockstep.debug = Rails.logger
225
243
  ```
226
244
 
245
+ ### Logging in the browser only
246
+
247
+ To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
248
+
249
+ ```
250
+ capybara_lockstep(debug: true)
251
+ ```
252
+
227
253
 
228
254
  ## Disabling synchronization
229
255
 
@@ -286,7 +312,7 @@ doAsynchronousWork().then(function() {
286
312
  })
287
313
  ```
288
314
 
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:
315
+ The string argument is used for logging (when logging is enabled). It does **not** need to be unique per job. In this case you should see messages like this in your browser's JavaScript console:
290
316
 
291
317
  ```text
292
318
  [capybara-lockstep] Started work: Eject warp core [1 jobs]
@@ -300,12 +326,35 @@ You may omit the string argument, in which case nothing will be logged, but the
300
326
 
301
327
  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
328
 
303
- ```
329
+ ```js
304
330
  if (window.CapybaraLockstep) {
305
331
  // interact with CapybaraLockstep
306
332
  }
307
333
  ```
308
334
 
335
+ ## Handling legacy promises
336
+
337
+ Legacy promise implementations (like jQuery's `$.Deferred` and AngularJS' `$q`) work using tasks instead of microtasks. Their AJAX implementations (like `$.ajax()` and `$http`) use these promises to signal that a request is done.
338
+
339
+ This means there is a time window in which all AJAX requests have finished, but their callbacks have not yet run:
340
+
341
+ ```js
342
+ $.ajax('/foo').then(function() {
343
+ // This callback runs one task after the response was received
344
+ })
345
+ ```
346
+
347
+ It is theoretically possible that your test will observe the browser in that window, and expect content that has not been rendered yet. This will usually be mitigated by Capybara's retry logic. **If** you think that this is an issue for your test suite, you can configure capybara-headless to wait additional tasks before it considers the browser to be idle:
348
+
349
+ ```js
350
+ Capybara:Lockstep.wait_tasks = 1
351
+ ```
352
+
353
+ If you see longer `then()` chains in your code, you may need to configure a higher number of tasks to wait.
354
+
355
+ This will have a negative performance impact on your test suite.
356
+
357
+
309
358
  ## Development
310
359
 
311
360
  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.
@@ -15,25 +15,16 @@ module Capybara
15
15
  @debug.nil? ? false : !!@debug
16
16
  end
17
17
 
18
- def debug=(debug)
19
- @debug = debug
20
- if debug
21
- target_prose = (is_logger?(debug) ? 'Ruby logger' : 'STDOUT')
18
+ def debug=(value)
19
+ @debug = value
20
+ if value
21
+ target_prose = (is_logger?(value) ? 'Ruby logger' : 'STDOUT')
22
22
  log "Logging to #{target_prose} and browser console"
23
23
  end
24
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
25
+ send_config_to_browser(<<~JS)
26
+ CapybaraLockstep.debug = #{value.to_json}
27
+ JS
37
28
 
38
29
  @debug
39
30
  end
@@ -50,6 +41,20 @@ module Capybara
50
41
  @enabled = enabled
51
42
  end
52
43
 
44
+ def wait_tasks
45
+ @wait_tasks
46
+ end
47
+
48
+ def wait_tasks=(value)
49
+ @wait_tasks = value
50
+
51
+ send_config_to_browser(<<~JS)
52
+ CapybaraLockstep.waitTasks = #{value.to_json}
53
+ JS
54
+
55
+ @wait_tasks
56
+ end
57
+
53
58
  def disabled?
54
59
  !enabled?
55
60
  end
@@ -60,6 +65,21 @@ module Capybara
60
65
  driver.is_a?(Capybara::Selenium::Driver)
61
66
  end
62
67
 
68
+ def send_config_to_browser(js)
69
+ begin
70
+ with_max_wait_time(2) do
71
+ page.execute_script(<<~JS)
72
+ if (window.CapybaraLockstep) {
73
+ #{js}
74
+ }
75
+ JS
76
+ end
77
+ rescue StandardError => e
78
+ log "#{e.class.name} while configuring capybara-lockstep in browser: #{e.message}"
79
+ # Don't fail. The next page load will include the snippet with the new config.
80
+ end
81
+ end
82
+
63
83
  end
64
84
  end
65
85
  end
@@ -1,13 +1,14 @@
1
1
  window.CapybaraLockstep = (function() {
2
- let count = 0
2
+ let jobCount = 0
3
3
  let idleCallbacks = []
4
4
  let debug = false
5
+ let waitTasks = 0
5
6
 
6
7
  function isIdle() {
7
8
  // Can't check for document.readyState or body.initializing here,
8
9
  // since the user might navigate away from the page before it finishes
9
10
  // initializing.
10
- return count === 0
11
+ return jobCount === 0
11
12
  }
12
13
 
13
14
  function isBusy() {
@@ -33,9 +34,9 @@ window.CapybaraLockstep = (function() {
33
34
  }
34
35
 
35
36
  function startWork(tag) {
36
- count++
37
+ jobCount++
37
38
  if (tag) {
38
- logNegative('Started work: %s [%d jobs]', tag, count)
39
+ logNegative('Started work: %s [%d jobs]', tag, jobCount)
39
40
  }
40
41
  }
41
42
 
@@ -44,23 +45,31 @@ window.CapybaraLockstep = (function() {
44
45
  promise.then(stopWork, stopWork)
45
46
  }
46
47
 
47
- function startWorkForMicrotask(tag) {
48
- startWork(tag)
49
- Promise.resolve().then(stopWork.bind(this, tag))
48
+ function stopWork(tag) {
49
+ let tasksElapsed = 0
50
+
51
+ let check = function() {
52
+ if (tasksElapsed < waitTasks) {
53
+ tasksElapsed++
54
+ setTimeout(check)
55
+ } else {
56
+ stopWorkNow(tag)
57
+ }
58
+ }
59
+
60
+ check()
50
61
  }
51
62
 
52
- function stopWork(tag) {
53
- count--
63
+ function stopWorkNow(tag) {
64
+ jobCount--
54
65
 
55
66
  if (tag) {
56
- logPositive('Finished work: %s [%d jobs]', tag, count)
67
+ logPositive('Finished work: %s [%d jobs]', tag, jobCount)
57
68
  }
58
69
 
59
- if (isIdle()) {
60
- idleCallbacks.forEach(function(callback) {
61
- callback('Finished waiting for JavaScript')
62
- })
63
- idleCallbacks = []
70
+ let idleCallback
71
+ while (isIdle() && (idleCallback = idleCallbacks.shift())) {
72
+ idleCallback('Finished waiting for JavaScript')
64
73
  }
65
74
  }
66
75
 
@@ -118,12 +127,16 @@ window.CapybaraLockstep = (function() {
118
127
  }
119
128
 
120
129
  function onInteraction(event) {
121
- // We wait until the end of this microtask, assuming that any callback that
122
- // would queue an AJAX request or load additional scripts will run by then.
123
- startWorkForMicrotask()
130
+ startWork()
131
+ // (1) We wait until the end of this microtask, assuming that any callback that
132
+ // would queue an AJAX request or load additional scripts will run by then.
133
+ // (2) For performance reasons we don't wait for `waitTasks` here.
134
+ // Whatever was queued by an event handler should call us again, and then
135
+ // we do wait for additional tasks.
136
+ Promise.resolve().then(stopWorkNow)
124
137
  }
125
138
 
126
- function trackDynamicScripts() {
139
+ function trackRemoteElements() {
127
140
  if (!window.MutationObserver) {
128
141
  return
129
142
  }
@@ -138,13 +151,16 @@ window.CapybaraLockstep = (function() {
138
151
  function trackJQuery() {
139
152
  // jQuery may be loaded after us, so we wait until DOMContentReady.
140
153
  whenReady(function() {
141
- if (!window.jQuery) {
154
+ if (!window.jQuery || waitTasks > 0) {
142
155
  return
143
156
  }
144
157
 
145
158
  // Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
146
159
  // not resolve in the next microtask but in the next *task* (it makes itself
147
160
  // async using setTimoeut()). Hence we need to wait for it in addition to XHR.
161
+ //
162
+ // If user code also uses $.Deferred(), it is also recommended to set
163
+ // CapybaraLockdown.waitTasks = 1 or higher.
148
164
  let oldAjax = window.jQuery.ajax
149
165
  window.jQuery.ajax = function() {
150
166
  let promise = oldAjax.apply(this, arguments)
@@ -161,7 +177,7 @@ window.CapybaraLockstep = (function() {
161
177
  // we consider ourselves busy.
162
178
  startWork()
163
179
  whenReady(function() {
164
- stopWork()
180
+ stopWorkNow()
165
181
  if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
166
182
  startWork('Page initialization')
167
183
  let observer = new MutationObserver(onInitializingAttributeChanged)
@@ -176,34 +192,97 @@ window.CapybaraLockstep = (function() {
176
192
  }
177
193
  }
178
194
 
179
- function isRemoteScript(node) {
180
- if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
181
- let src = node.getAttribute('src')
182
- let type = node.getAttribute('type')
195
+ function isRemoteScript(element) {
196
+ if (element.tagName === 'SCRIPT') {
197
+ let src = element.getAttribute('src')
198
+ let type = element.getAttribute('type')
199
+
200
+ return src && (!type || /javascript/i.test(type))
201
+ }
202
+ }
203
+
204
+ function isRemoteImage(element) {
205
+ if (element.tagName === 'IMG' && !element.complete) {
206
+ let src = element.getAttribute('src')
207
+ let srcSet = element.getAttribute('srcset')
208
+
209
+ let localSrcPattern = /^data:/
210
+ let localSrcSetPattern = /(^|\s)data:/
211
+
212
+ let hasLocalSrc = src && localSrcPattern.test(src)
213
+ let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
214
+
215
+ return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
216
+ }
217
+ }
183
218
 
184
- return (src && (!type || /javascript/i.test(type)))
219
+ function isRemoteInlineFrame(element) {
220
+ if (element.tagName === 'IFRAME') {
221
+ let src = element.getAttribute('src')
222
+ let localSrcPattern = /^data:/
223
+ let hasLocalSrc = src && localSrcPattern.test(src)
224
+ return (src && !hasLocalSrc)
185
225
  }
186
226
  }
187
227
 
188
- function onRemoteScriptAdded(script) {
189
- let workTag = 'Remote script ' + script.getAttribute('src')
228
+ function trackRemoteElement(element, condition, workTag) {
229
+ if (!condition(element)) {
230
+ return
231
+ }
232
+
233
+ let stopped = false
234
+
190
235
  startWork(workTag)
191
- let taggedStopWork = stopWork.bind(this, workTag)
192
- // Chrome runs a remote <script> *before* the load event fires.
193
- script.addEventListener('load', taggedStopWork)
194
- script.addEventListener('error', taggedStopWork)
236
+
237
+ let doStop = function() {
238
+ stopped = true
239
+ element.removeEventListener('load', doStop)
240
+ element.removeEventListener('error', doStop)
241
+ stopWork(workTag)
242
+ }
243
+
244
+ let checkCondition = function() {
245
+ if (stopped) {
246
+ // A `load` or `error` event has fired.
247
+ // We can stop here. No need to schedule another check.
248
+ return
249
+ } else if (isDetached(element) || !condition(element)) {
250
+ // If it is detached or if its `[src]` attribute changes to a data: URL
251
+ // we may never get a `load` or `error` event.
252
+ doStop()
253
+ } else {
254
+ scheduleCheckCondition()
255
+ }
256
+ }
257
+
258
+ let scheduleCheckCondition = function() {
259
+ setTimeout(checkCondition, 200)
260
+ }
261
+
262
+ element.addEventListener('load', doStop)
263
+ element.addEventListener('error', doStop)
264
+
265
+ // We periodically check whether we still think the element will
266
+ // produce a `load` or `error` event.
267
+ scheduleCheckCondition()
195
268
  }
196
269
 
197
270
  function onAnyElementChanged(changes) {
198
271
  changes.forEach(function(change) {
199
272
  change.addedNodes.forEach(function(addedNode) {
200
- if (isRemoteScript(addedNode)) {
201
- onRemoteScriptAdded(addedNode)
273
+ if (addedNode.nodeType === Node.ELEMENT_NODE) {
274
+ trackRemoteElement(addedNode, isRemoteScript, 'Script')
275
+ trackRemoteElement(addedNode, isRemoteImage, 'Image')
276
+ trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
202
277
  }
203
278
  })
204
279
  })
205
280
  }
206
281
 
282
+ function isDetached(element) {
283
+ return !document.contains(element)
284
+ }
285
+
207
286
  function whenReady(callback) {
208
287
  // Values are "loading", "interactive" and "completed".
209
288
  // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
@@ -218,7 +297,7 @@ window.CapybaraLockstep = (function() {
218
297
  trackFetch()
219
298
  trackXHR()
220
299
  trackInteraction()
221
- trackDynamicScripts()
300
+ trackRemoteElements()
222
301
  trackJQuery()
223
302
  trackHydration()
224
303
  }
@@ -236,9 +315,8 @@ window.CapybaraLockstep = (function() {
236
315
  startWork: startWork,
237
316
  stopWork: stopWork,
238
317
  synchronize: synchronize,
239
- set debug(newDebug) {
240
- debug = newDebug
241
- }
318
+ set debug(value) { debug = value },
319
+ set waitTasks(value) { waitTasks = value }
242
320
  }
243
321
  })()
244
322
 
@@ -17,13 +17,22 @@ module Capybara
17
17
  tag_options[:nonce] = options.fetch(:nonce, true)
18
18
  end
19
19
 
20
- full_js = capybara_lockstep_js
20
+ js = capybara_lockstep_js + capybara_lockstep_config_js(options)
21
+ javascript_tag(js, tag_options)
22
+ end
23
+
24
+ def capybara_lockstep_config_js(options = {})
25
+ js = ''
21
26
 
22
27
  if (debug = options.fetch(:debug, Lockstep.debug?))
23
- full_js += "\nCapybaraLockstep.debug = #{debug.to_json}"
28
+ js += "\nCapybaraLockstep.debug = #{debug.to_json}"
29
+ end
30
+
31
+ if (wait_tasks = options.fetch(:wait_tasks, Lockstep.wait_tasks))
32
+ js += "\nCapybaraLockstep.waitTasks = #{wait_tasks.to_json}"
24
33
  end
25
34
 
26
- javascript_tag(full_js, tag_options)
35
+ js
27
36
  end
28
37
 
29
38
  end
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "0.4.0"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  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.0
4
+ version: 0.5.0
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-05 00:00:00.000000000 Z
11
+ date: 2021-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara