capybara-lockstep 0.3.3 → 1.0.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: 86f6aba93856539de9e2df301e720cced0c444de2d62436e72ea620765f9b3e1
4
+ data.tar.gz: 1ae5dc301b4761d7c51796313ea4a6210596b5975e02f639410916ddc53060b5
5
5
  SHA512:
6
- metadata.gz: 846816f82797d02dc81869e449afaf99c3fdfe89682d6584a56b3416cbb5942138a38ae457dfebcbb4fc7f20ed2673766a02287911b258445c5a9b800d613f9c
7
- data.tar.gz: 30064e290e2caed298e5868b0f1cb9c1f5aa34444895fa8cefd4d283b0433528dd1ee7793526f0652fbc11511ed3e50e9a6eb999b00f6a898411480616422199
6
+ metadata.gz: b1dc1f1e4a8af3d4d02f4d7b2aec3ae99844ba42bd3713028a138842bdc296c6bc0ed39688b8c15d7976830cc97df9ebc96cee3f3cfc9ab5f799e99ec59fee23
7
+ data.tar.gz: 50e27f5304cc7fbc1ed0c74ee87311ef8c8a333ecd4395af7cf155305ce0607f4b0f20defdf901d8f5cb3cbe8fc1ddb23638d9587c27f15e2071aea59c24297c
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: Tests
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ branches:
9
+ - master
10
+ workflow_dispatch:
11
+ branches:
12
+ - master
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-20.04
16
+ timeout-minutes: 3
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ include:
21
+ - ruby: 2.6.6
22
+ gemfile: Gemfile
23
+ - ruby: 2.7.2
24
+ gemfile: Gemfile
25
+ - ruby: 3.0.1
26
+ gemfile: Gemfile
27
+ env:
28
+ BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
29
+ steps:
30
+ - uses: actions/checkout@v2
31
+ - name: Install ruby
32
+ uses: ruby/setup-ruby@v1
33
+ with:
34
+ ruby-version: "${{ matrix.ruby }}"
35
+ - name: Bundle
36
+ run: |
37
+ gem install bundler:2.2.15
38
+ bundle install --no-deployment
39
+ - name: Run tests
40
+ uses: nick-invision/retry@v2
41
+ with:
42
+ timeout_seconds: 30
43
+ max_attempts: 3
44
+ command: bundle exec rake spec
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ .idea
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ All notable changes to this project will be documented in this file.
2
+
3
+ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
+
5
+
6
+ ## Unreleased
7
+
8
+ ### Breaking changes
9
+
10
+ -
11
+
12
+ ### Compatible changes
13
+
14
+ -
15
+
16
+ ## 0.7.0 - 2021-05-04
17
+
18
+ ### Compatible changes
19
+
20
+ - add changelog
21
+ - add gemika for tests with github actions
22
+ - add Ruby 3 support
23
+
24
+ ## 0.6.0 - 2021-03-10
25
+ ## 0.5.0 - 2021-03-09
26
+ ## 0.4.0 - 2021-03-05
27
+ ## 0.3.3 - 2021-03-05
28
+ ## 0.3.2 - 2021-03-04
29
+ ## 0.3.1 - 2021-03-04
30
+ ## 0.3.0 - 2021-03-04
31
+ ## 0.2.3 - 2021-03-03
32
+ ## 0.2.2 - 2021-03-03
33
+ ## 0.2.1 - 2021-03-03
data/Gemfile CHANGED
@@ -8,5 +8,9 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+ gem 'jasmine'
12
+ gem 'thin' # ruby 3 does not include a webserver
13
+ gem 'chrome_remote'
11
14
 
12
15
  gem 'byebug'
16
+ gem 'gemika'
data/Gemfile.lock CHANGED
@@ -1,15 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (0.3.3)
4
+ capybara-lockstep (1.0.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
+ ruby2_keywords
7
8
  selenium-webdriver (>= 3)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
12
- activesupport (6.1.3)
13
+ activesupport (6.1.3.1)
13
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
15
  i18n (>= 1.6, < 2)
15
16
  minitest (>= 5.1)
@@ -27,16 +28,26 @@ GEM
27
28
  regexp_parser (>= 1.5, < 3.0)
28
29
  xpath (~> 3.2)
29
30
  childprocess (3.0.0)
31
+ chrome_remote (0.3.0)
32
+ websocket-driver (~> 0.6)
30
33
  concurrent-ruby (1.1.8)
34
+ daemons (1.3.1)
31
35
  diff-lcs (1.3)
32
- i18n (1.8.9)
36
+ eventmachine (1.2.7)
37
+ gemika (0.6.0)
38
+ i18n (1.8.10)
33
39
  concurrent-ruby (~> 1.0)
34
- mini_mime (1.0.2)
35
- mini_portile2 (2.5.0)
40
+ jasmine (3.6.0)
41
+ jasmine-core (~> 3.6.0)
42
+ phantomjs
43
+ rack (>= 1.2.1)
44
+ rake
45
+ jasmine-core (3.6.0)
46
+ mini_mime (1.1.0)
36
47
  minitest (5.14.4)
37
- nokogiri (1.11.1)
38
- mini_portile2 (~> 2.5.0)
48
+ nokogiri (1.11.3-x86_64-linux)
39
49
  racc (~> 1.4)
50
+ phantomjs (2.1.1.0)
40
51
  public_suffix (4.0.6)
41
52
  racc (1.5.2)
42
53
  rack (2.2.3)
@@ -57,12 +68,20 @@ GEM
57
68
  diff-lcs (>= 1.2.0, < 2.0)
58
69
  rspec-support (~> 3.7.0)
59
70
  rspec-support (3.7.0)
71
+ ruby2_keywords (0.0.4)
60
72
  rubyzip (2.3.0)
61
73
  selenium-webdriver (3.142.7)
62
74
  childprocess (>= 0.5, < 4.0)
63
75
  rubyzip (>= 1.2.2)
76
+ thin (1.8.0)
77
+ daemons (~> 1.0, >= 1.0.9)
78
+ eventmachine (~> 1.0, >= 1.0.4)
79
+ rack (>= 1, < 3)
64
80
  tzinfo (2.0.4)
65
81
  concurrent-ruby (~> 1.0)
82
+ websocket-driver (0.7.3)
83
+ websocket-extensions (>= 0.1.0)
84
+ websocket-extensions (0.1.5)
66
85
  xpath (3.2.0)
67
86
  nokogiri (~> 1.8)
68
87
  zeitwerk (2.4.2)
@@ -73,8 +92,12 @@ PLATFORMS
73
92
  DEPENDENCIES
74
93
  byebug
75
94
  capybara-lockstep!
95
+ chrome_remote
96
+ gemika
97
+ jasmine
76
98
  rake (~> 13.0)
77
99
  rspec (~> 3.0)
100
+ thin
78
101
 
79
102
  BUNDLED WITH
80
- 2.2.12
103
+ 2.2.15
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,41 @@ 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
  })
136
137
  })
137
138
  ```
138
139
 
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]`:
140
+ 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
141
 
141
142
  ```js
142
143
  document.addEventListener('DOMContentLoaded', function() {
143
- Libary.doWorkInNextTask()
144
+ Libary.initialize()
144
145
  setTimeout(function() { document.body.removeAttribute('data-initializing') })
145
146
  })
146
147
  ```
147
148
 
149
+ If your initialization code lazy-loads another script, you should only remove `[data-initializing]` once that is done:
150
+
151
+ ```js
152
+ document.addEventListener('DOMContentLoaded', function() {
153
+ import('huge-library').then(function({ HugeLibrary }) {
154
+ HugeLibrary.initialize()
155
+ document.body.removeAttribute('data-initializing')
156
+ })
157
+ })
158
+ ```
159
+
160
+
161
+ #### Example: Unpoly
162
+
148
163
  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
164
 
150
165
  ```js
@@ -153,6 +168,8 @@ up.compiler('body', function(body) {
153
168
  })
154
169
  ```
155
170
 
171
+ #### Example: AngularJS 1
172
+
156
173
  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
174
 
158
175
  ```js
@@ -192,13 +209,15 @@ In casual testing I experienced a performance impact between +/- 10%.
192
209
 
193
210
  ## Debugging log
194
211
 
195
- capybara-lockstep can print to the console whenever it waits for the browser. To enable the log:
212
+ 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.
213
+
214
+ To enable the log, say this before or during a test:
196
215
 
197
216
  ```ruby
198
217
  Capybara::Lockstep.debug = true
199
218
  ```
200
219
 
201
- You should now see messages like this during your test runs:
220
+ You should now see messages like this on your standard output:
202
221
 
203
222
  ```
204
223
  [capybara-lockstep] Synchronizing
@@ -206,29 +225,35 @@ You should now see messages like this during your test runs:
206
225
  [capybara-lockstep] Synchronized successfully
207
226
  ```
208
227
 
228
+ You should also see messages like this in your browser's JavaScript console:
229
+
230
+ ```
231
+ [capybara-lockstep] Started work: fetch /path [3 jobs]
232
+ [capybara-lockstep] Finished work: fetch /path [2 jobs]
233
+ ```
234
+
235
+
236
+ ### Using a logger
237
+
209
238
  You may also configure logging to an existing logger object:
210
239
 
211
240
  ```ruby
212
241
  Capybara::Lockstep.debug = Rails.logger
213
242
  ```
214
243
 
244
+ ### Logging in the browser only
215
245
 
216
- ## Disabling synchronization
217
-
218
- If for some reason you want to disable browser synchronization for a while, you can do it like this:
246
+ To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
219
247
 
220
- ```ruby
221
- begin
222
- Capybara::Lockstep.enabled = false
223
- do_unsynchronized_work
224
- ensure
225
- Capybara::Lockstep.enabled = true
226
- end
227
248
  ```
249
+ capybara_lockstep(debug: true)
250
+ ```
251
+
252
+ ## Synchronization timeout
228
253
 
229
- ## Timeout
254
+ 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
255
 
231
- By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
256
+ When synchronization times out, capybara-lockstep will log but not raise an error.
232
257
 
233
258
  You can configure a different timeout:
234
259
 
@@ -236,71 +261,109 @@ You can configure a different timeout:
236
261
  Capybara::Lockstep.timeout = 5 # seconds
237
262
  ```
238
263
 
239
- ## Ruby API
264
+ To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
240
265
 
241
- capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
266
+ ```ruby
267
+ Capybara::Lockstep.timeout = nil
268
+ ```
242
269
 
243
- For additional edge cases you may interact with capybara-lockstep from your Ruby code.
244
270
 
271
+ ## Manual synchronization
245
272
 
246
- ### Waiting until the browser is idle
273
+ capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
247
274
 
248
- This will block until the document was loaded, the DOM has been hydrated and all AJAX requests have concluded:
275
+ For additional edge cases you may manually tell capybara-lockstep to wait. The following Ruby method will block until the browser is idle:
249
276
 
250
277
  ```ruby
251
278
  Capybara::Lockstep.synchronize
252
279
  ```
253
280
 
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:
281
+ You may also synchronize from your client-side JavaScript. The following will run the given callback once the browser is idle:
255
282
 
256
- ```gherkin
257
- When 'I wait for the page to load' do
258
- Capybara::Lockstep.synchronize
259
- end
283
+ ```js
284
+ CapybaraLockstep.synchronize(callback)
260
285
  ```
261
286
 
262
- ## JavaScript API
263
287
 
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**.
288
+ ## Disabling synchronization
265
289
 
266
- For additional edge cases you may interact with capybara-lockstep from your own JavaScripts.
290
+ Sometimes you want to disable browser synchronization, e.g. to observe a loading spinner during a long-running request.
267
291
 
268
- Note that when you only load the JavaScript snippet in tests you need check before calling any API functions:
292
+ To disable automatic synchronization:
269
293
 
270
- ```js
271
- if (window.CapybaraLockstep) {
272
- CapybaraLockstep.startWork()
273
- }
294
+ ```ruby
295
+ begin
296
+ Capybara::Lockstep.mode = :manual
297
+ do_unsynchronized_work
298
+ ensure
299
+ Capybara::Lockstep.mode = :auto
300
+ end
301
+ ```
302
+
303
+ Note that you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
304
+
305
+ To completely disable synchronization:
306
+
307
+ ```ruby
308
+ Capybara::Lockstep.mode = :off
309
+ Capybara::Lockstep.synchronize # will not synchronize
274
310
  ```
275
311
 
276
- ### Signaling asynchronous work
312
+
313
+ ## Signaling asynchronous work
277
314
 
278
315
  If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
279
316
 
280
317
  ```js
281
- CapybaraLockstep.startWork()
318
+ CapybaraLockstep.startWork('Eject warp core')
282
319
  doAsynchronousWork().then(function() {
283
- CapybaraLockstep.stopWork()
320
+ CapybaraLockstep.stopWork('Eject warp core')
284
321
  })
285
322
  ```
286
323
 
287
- ### Checking if the browser is busy
324
+ 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:
325
+
326
+ ```text
327
+ [capybara-lockstep] Started work: Eject warp core [1 jobs]
328
+ [capybara-lockstep] Finished work: Eject warp core [0 jobs]
329
+ ```
330
+
331
+ You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
288
332
 
289
- You can query capybara-lockstep whether it considers the browser to be busy or idle:
333
+
334
+ ## Note on interacting with the JavaScript API
335
+
336
+ 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.
290
337
 
291
338
  ```js
292
- CapybaraLockstep.isBusy() // => false
293
- CapybaraLockstep.isIdle() // => true
339
+ if (window.CapybaraLockstep) {
340
+ // interact with CapybaraLockstep
341
+ }
294
342
  ```
295
343
 
296
- ### Waiting until the browser is idle
344
+ ## Handling legacy promises
345
+
346
+ 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.
297
347
 
298
- This will run the given callback once the browser is considered to be idle:
348
+ This means there is a time window in which all AJAX requests have finished, but their callbacks have not yet run:
299
349
 
300
350
  ```js
301
- CapybaraLockstep.synchronize(callback)
351
+ $.ajax('/foo').then(function() {
352
+ // This callback runs one task after the response was received
353
+ })
302
354
  ```
303
355
 
356
+ 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:
357
+
358
+ ```js
359
+ Capybara:Lockstep.wait_tasks = 1
360
+ ```
361
+
362
+ If you see longer `then()` chains in your code, you may need to configure a higher number of tasks to wait.
363
+
364
+ This will have a negative performance impact on your test suite.
365
+
366
+
304
367
  ## Development
305
368
 
306
369
  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.
data/Rakefile CHANGED
@@ -6,3 +6,11 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
+ require 'jasmine'
10
+ load 'jasmine/tasks/jasmine.rake'
11
+
12
+ begin
13
+ require 'gemika/tasks'
14
+ rescue LoadError
15
+ puts 'Run `gem install gemika` for additional tasks'
16
+ end
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency "capybara", ">= 2.0"
28
28
  spec.add_dependency "selenium-webdriver", ">= 3"
29
29
  spec.add_dependency "activesupport", ">= 3.2"
30
+ spec.add_dependency "ruby2_keywords"
30
31
 
31
32
  # For more information and examples about making a new gem, checkout our
32
33
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -1,7 +1,9 @@
1
+ require 'ruby2_keywords'
2
+
1
3
  module Capybara
2
4
  module Lockstep
3
5
  module VisitWithWaiting
4
- def visit(*args, &block)
6
+ ruby2_keywords def visit(*args, &block)
5
7
  url = args[0]
6
8
  # Some of our apps have a Cucumber step that changes drivers mid-scenario.
7
9
  # It works by creating a new Capybara session and re-visits the URL from the
@@ -12,13 +14,18 @@ module Capybara
12
14
 
13
15
  if visiting_remote_url
14
16
  # We're about to leave this screen, killing all in-flight requests.
15
- Capybara::Lockstep.synchronize
17
+ # Give pending form submissions etc. a chance to finish before we tear down
18
+ # the browser environment.
19
+ #
20
+ # We force a non-lazy synchronization so we pick up all client-side changes
21
+ # that have not been caused by Capybara commands.
22
+ Lockstep.synchronize(lazy: false)
16
23
  end
17
24
 
18
25
  super(*args, &block).tap do
19
26
  if visiting_remote_url
20
- # puts "After visit: unsynchronizing"
21
- Capybara::Lockstep.synchronized = false
27
+ # We haven't yet synchronized the new screen.
28
+ Lockstep.synchronized = false
22
29
  end
23
30
  end
24
31
  end
@@ -26,11 +33,57 @@ module Capybara
26
33
  end
27
34
  end
28
35
 
29
-
30
36
  Capybara::Session.class_eval do
31
37
  prepend Capybara::Lockstep::VisitWithWaiting
32
38
  end
33
39
 
40
+ module Capybara
41
+ module Lockstep
42
+ module SynchronizeAroundScriptMethod
43
+
44
+ def synchronize_around_script_method(meth)
45
+ mod = Module.new do
46
+ define_method meth do |script, *args, &block|
47
+ # Synchronization uses execute_script itself, so don't synchronize when
48
+ # we're already synchronizing.
49
+ if !Lockstep.synchronizing?
50
+ # It's generally a good idea to synchronize before a JavaScript wants
51
+ # to access or observe an earlier state change.
52
+ #
53
+ # In case the given script navigates away (with `location.href = url`,
54
+ # `history.back()`, etc.) we would kill all in-flight requests. For this case
55
+ # we force a non-lazy synchronization so we pick up all client-side changes
56
+ # that have not been caused by Capybara commands.
57
+ script_may_navigate_away = script =~ /\b(location|history)\b/
58
+ Lockstep.auto_synchronize(lazy: !script_may_navigate_away, log: "Synchronizing before script: #{script}")
59
+ end
60
+
61
+ super(script, *args, &block).tap do
62
+ if !Lockstep.synchronizing?
63
+ # We haven't yet synchronized with whatever changes the JavaScript
64
+ # did on the frontend.
65
+ Lockstep.synchronized = false
66
+ end
67
+ end
68
+ end
69
+ ruby2_keywords meth
70
+ end
71
+ prepend(mod)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+
78
+ Capybara::Session.class_eval do
79
+ extend Capybara::Lockstep::SynchronizeAroundScriptMethod
80
+
81
+ synchronize_around_script_method :execute_script
82
+ synchronize_around_script_method :evaluate_async_script
83
+ # Don't synchronize around evaluate_script. It calls execute_script
84
+ # internally and we don't want to synchronize multiple times.
85
+ end
86
+
34
87
  module Capybara
35
88
  module Lockstep
36
89
  module UnsychronizeAfter
@@ -38,9 +91,10 @@ module Capybara
38
91
  mod = Module.new do
39
92
  define_method meth do |*args, &block|
40
93
  super(*args, &block).tap do
41
- Capybara::Lockstep.synchronized = false
94
+ Lockstep.synchronized = false
42
95
  end
43
96
  end
97
+ ruby2_keywords meth
44
98
  end
45
99
  prepend(mod)
46
100
  end
@@ -87,10 +141,11 @@ end
87
141
  module Capybara
88
142
  module Lockstep
89
143
  module SynchronizeWithCatchUp
90
- def synchronize(*args, &block)
91
- # This method is called very frequently by capybara.
144
+ ruby2_keywords def synchronize(*args, &block)
145
+ # This method is called by Capybara before most interactions with
146
+ # the browser. It is a different method than Capybara::Lockstep.synchronize!
92
147
  # We use the { lazy } option to only synchronize when we're out of sync.
93
- Capybara::Lockstep.synchronize(lazy: true)
148
+ Capybara::Lockstep.auto_synchronize(lazy: true)
94
149
 
95
150
  super(*args, &block)
96
151
  end
@@ -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,27 +11,62 @@ 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
- def debug=(debug)
18
- @debug = debug
18
+ def debug=(value)
19
+ @debug = value
20
+ if value
21
+ target_prose = (is_logger?(value) ? 'Ruby logger' : 'STDOUT')
22
+ log "Logging to #{target_prose} and browser console"
23
+ end
24
+
25
+ send_config_to_browser(<<~JS)
26
+ CapybaraLockstep.debug = #{value.to_json}
27
+ JS
28
+
29
+ @debug
19
30
  end
20
31
 
21
- def enabled?
32
+ def mode
22
33
  if javascript_driver?
23
- @enabled.nil? ? true : @enabled
34
+ @mode.nil? ? :auto : @mode
24
35
  else
25
- false
36
+ :off
26
37
  end
27
38
  end
28
39
 
40
+ def mode=(mode)
41
+ @mode = mode
42
+ end
43
+
29
44
  def enabled=(enabled)
30
- @enabled = enabled
45
+ case enabled
46
+ when true
47
+ log "Setting `Capybara::Lockstep.enabled = true` is deprecated. Set `Capybara::Lockstep.mode = :auto` instead."
48
+ self.mode = :auto
49
+ when false
50
+ log "Setting `Capybara::Lockstep.enabled = false` is deprecated. Set `Capybara::Lockstep.mode = :manual` or `Capybara::Lockstep.mode = :off` instead."
51
+ self.mode = :manual
52
+ when nil
53
+ # Reset to default
54
+ self.mode = nil
55
+ end
31
56
  end
32
57
 
33
- def disabled?
34
- !enabled?
58
+ def wait_tasks
59
+ @wait_tasks
60
+ end
61
+
62
+ def wait_tasks=(value)
63
+ @wait_tasks = value
64
+
65
+ send_config_to_browser(<<~JS)
66
+ CapybaraLockstep.waitTasks = #{value.to_json}
67
+ JS
68
+
69
+ @wait_tasks
35
70
  end
36
71
 
37
72
  private
@@ -40,6 +75,21 @@ module Capybara
40
75
  driver.is_a?(Capybara::Selenium::Driver)
41
76
  end
42
77
 
78
+ def send_config_to_browser(js)
79
+ begin
80
+ with_max_wait_time(2) do
81
+ page.execute_script(<<~JS)
82
+ if (window.CapybaraLockstep) {
83
+ #{js}
84
+ }
85
+ JS
86
+ end
87
+ rescue StandardError => e
88
+ log "#{e.class.name} while configuring capybara-lockstep in browser: #{e.message}"
89
+ # Don't fail. The next page load will include the snippet with the new config.
90
+ end
91
+ end
92
+
43
93
  end
44
94
  end
45
95
  end
@@ -1,45 +1,85 @@
1
1
  window.CapybaraLockstep = (function() {
2
- var count = 0
3
- var idleCallbacks = []
2
+ // State and configuration
3
+ let debug
4
+ let jobCount
5
+ let idleCallbacks
6
+ let waitTasks
7
+ reset()
8
+
9
+ function reset() {
10
+ jobCount = 0
11
+ idleCallbacks = []
12
+ waitTasks = 0
13
+ debug = false
14
+ }
4
15
 
5
16
  function isIdle() {
6
17
  // Can't check for document.readyState or body.initializing here,
7
18
  // since the user might navigate away from the page before it finishes
8
19
  // initializing.
9
- return count === 0
20
+ return jobCount === 0
10
21
  }
11
22
 
12
23
  function isBusy() {
13
24
  return !isIdle()
14
25
  }
15
26
 
16
- function startWork() {
17
- count++
27
+ function log(...args) {
28
+ if (debug) {
29
+ args[0] = '%c[capybara-lockstep] ' + args[0]
30
+ args.splice(1, 0, 'color: #666666')
31
+ console.log.apply(console, args)
32
+ }
18
33
  }
19
34
 
20
- function startWorkUntil(promise) {
21
- startWork()
22
- promise.then(stopWork, stopWork)
35
+ function logPositive(...args) {
36
+ args[0] = '%c' + args[0]
37
+ log(args[0], 'color: #117722', ...args.slice(1))
23
38
  }
24
39
 
25
- function startWorkForTime(time) {
26
- startWork()
27
- setTimeout(stopWork, time)
40
+ function logNegative(...args) {
41
+ args[0] = '%c' + args[0]
42
+ log(args[0], 'color: #cc3311', ...args.slice(1))
28
43
  }
29
44
 
30
- function startWorkForMicrotask() {
31
- startWork()
32
- Promise.resolve().then(stopWork)
45
+ function startWork(tag) {
46
+ jobCount++
47
+ if (tag) {
48
+ logNegative('Started work: %s [%d jobs]', tag, jobCount)
49
+ }
33
50
  }
34
51
 
35
- function stopWork() {
36
- count--
52
+ function startWorkUntil(promise, tag) {
53
+ startWork(tag)
54
+ let taggedStopWork = stopWork.bind(this, tag)
55
+ promise.then(taggedStopWork, taggedStopWork)
56
+ }
37
57
 
38
- if (isIdle()) {
39
- idleCallbacks.forEach(function(callback) {
40
- callback('Finished waiting for JavaScript')
41
- })
42
- idleCallbacks = []
58
+ function stopWork(tag) {
59
+ let tasksElapsed = 0
60
+
61
+ let check = function() {
62
+ if (tasksElapsed < waitTasks) {
63
+ tasksElapsed++
64
+ setTimeout(check)
65
+ } else {
66
+ stopWorkNow(tag)
67
+ }
68
+ }
69
+
70
+ check()
71
+ }
72
+
73
+ function stopWorkNow(tag) {
74
+ jobCount--
75
+
76
+ if (tag) {
77
+ logPositive('Finished work: %s [%d jobs]', tag, jobCount)
78
+ }
79
+
80
+ let idleCallback
81
+ while (isIdle() && (idleCallback = idleCallbacks.shift())) {
82
+ idleCallback('Finished waiting for browser')
43
83
  }
44
84
  }
45
85
 
@@ -48,29 +88,36 @@ window.CapybaraLockstep = (function() {
48
88
  return
49
89
  }
50
90
 
51
- var oldFetch = window.fetch
91
+ let oldFetch = window.fetch
52
92
  window.fetch = function() {
53
- var promise = oldFetch.apply(this, arguments)
54
- startWorkUntil(promise)
93
+ let promise = oldFetch.apply(this, arguments)
94
+ startWorkUntil(promise, 'fetch ' + arguments[0])
55
95
  return promise
56
96
  }
57
97
  }
58
98
 
59
99
  function trackXHR() {
60
- var oldSend = XMLHttpRequest.prototype.send
100
+ let oldOpen = XMLHttpRequest.prototype.open
101
+ let oldSend = XMLHttpRequest.prototype.send
102
+
103
+ XMLHttpRequest.prototype.open = function() {
104
+ this.capybaraLockstepURL = arguments[1]
105
+ return oldOpen.apply(this, arguments)
106
+ }
61
107
 
62
108
  XMLHttpRequest.prototype.send = function() {
63
- startWork()
109
+ let workTag = 'XHR to '+ this.capybaraLockstepURL
110
+ startWork(workTag)
64
111
 
65
112
  try {
66
113
  this.addEventListener('readystatechange', function(event) {
67
- if (this.readyState === 4) { stopWork() }
114
+ if (this.readyState === 4) { stopWork(workTag) }
68
115
  }.bind(this))
69
116
  return oldSend.apply(this, arguments)
70
117
  } catch (e) {
71
118
  // If we get a sync exception during request dispatch
72
119
  // we assume the request never went out.
73
- stopWork()
120
+ stopWork(workTag)
74
121
  throw e
75
122
  }
76
123
  }
@@ -89,13 +136,17 @@ window.CapybaraLockstep = (function() {
89
136
  })
90
137
  }
91
138
 
92
- function onInteraction() {
93
- // We wait until the end of this microtask, assuming that any callback that
94
- // would queue an AJAX request or load additional scripts will run by then.
95
- startWorkForMicrotask()
139
+ function onInteraction(event) {
140
+ startWork()
141
+ // (1) We wait until the end of this microtask, assuming that any callback that
142
+ // would queue an AJAX request or load additional scripts will run by then.
143
+ // (2) For performance reasons we don't wait for `waitTasks` here.
144
+ // Whatever was queued by an event handler should call us again, and then
145
+ // we do wait for additional tasks.
146
+ Promise.resolve().then(stopWorkNow)
96
147
  }
97
148
 
98
- function trackDynamicScripts() {
149
+ function trackRemoteElements() {
99
150
  if (!window.MutationObserver) {
100
151
  return
101
152
  }
@@ -103,40 +154,43 @@ window.CapybaraLockstep = (function() {
103
154
  // Dynamic imports or analytics snippets may insert a <script src>
104
155
  // tag that loads and executes additional JavaScript. We want to be isBusy()
105
156
  // until such scripts have loaded or errored.
106
- var observer = new MutationObserver(onAnyElementChanged)
157
+ let observer = new MutationObserver(onAnyElementChanged)
107
158
  observer.observe(document, { subtree: true, childList: true })
108
159
  }
109
160
 
110
161
  function trackJQuery() {
111
162
  // jQuery may be loaded after us, so we wait until DOMContentReady.
112
163
  whenReady(function() {
113
- if (!window.jQuery) {
164
+ if (!window.jQuery || waitTasks > 0) {
114
165
  return
115
166
  }
116
167
 
117
168
  // Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
118
169
  // not resolve in the next microtask but in the next *task* (it makes itself
119
170
  // 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)
171
+ //
172
+ // If user code also uses $.Deferred(), it is also recommended to set
173
+ // CapybaraLockdown.waitTasks = 1 or higher.
174
+ let oldAjax = window.jQuery.ajax
175
+ window.jQuery.ajax = function() {
176
+ let promise = oldAjax.apply(this, arguments)
123
177
  startWorkUntil(promise)
124
178
  return promise
125
179
  }
126
180
  })
127
181
  }
128
182
 
129
- var INITIALIZING_ATTRIBUTE = 'data-initializing'
183
+ let INITIALIZING_ATTRIBUTE = 'data-initializing'
130
184
 
131
185
  function trackHydration() {
132
186
  // Until we have a body on which we can observe [data-initializing]
133
187
  // we consider ourselves busy.
134
188
  startWork()
135
189
  whenReady(function() {
136
- stopWork()
190
+ stopWorkNow()
137
191
  if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
138
- startWork()
139
- var observer = new MutationObserver(onInitializingAttributeChanged)
192
+ startWork('Page initialization')
193
+ let observer = new MutationObserver(onInitializingAttributeChanged)
140
194
  observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
141
195
  }
142
196
  })
@@ -144,36 +198,101 @@ window.CapybaraLockstep = (function() {
144
198
 
145
199
  function onInitializingAttributeChanged() {
146
200
  if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
147
- stopWork()
201
+ stopWork('Page initialization')
148
202
  }
149
203
  }
150
204
 
151
- function isRemoteScript(node) {
152
- if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
153
- var src = node.getAttribute('src')
154
- var type = node.getAttribute('type')
205
+ function isRemoteScript(element) {
206
+ if (element.tagName === 'SCRIPT') {
207
+ let src = element.getAttribute('src')
208
+ let type = element.getAttribute('type')
155
209
 
156
- return (src && (!type || /javascript/i.test(type)))
210
+ return src && (!type || /javascript/i.test(type))
157
211
  }
158
212
  }
159
213
 
160
- function onRemoteScriptAdded(script) {
161
- startWork()
162
- // Chrome runs a remote <script> *before* the load event fires.
163
- script.addEventListener('load', stopWork)
164
- script.addEventListener('error', stopWork)
214
+ function isRemoteImage(element) {
215
+ if (element.tagName === 'IMG' && !element.complete) {
216
+ let src = element.getAttribute('src')
217
+ let srcSet = element.getAttribute('srcset')
218
+
219
+ let localSrcPattern = /^data:/
220
+ let localSrcSetPattern = /(^|\s)data:/
221
+
222
+ let hasLocalSrc = src && localSrcPattern.test(src)
223
+ let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
224
+
225
+ return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
226
+ }
227
+ }
228
+
229
+ function isRemoteInlineFrame(element) {
230
+ if (element.tagName === 'IFRAME') {
231
+ let src = element.getAttribute('src')
232
+ let localSrcPattern = /^data:/
233
+ let hasLocalSrc = src && localSrcPattern.test(src)
234
+ return (src && !hasLocalSrc)
235
+ }
236
+ }
237
+
238
+ function trackRemoteElement(element, condition, workTag) {
239
+ if (!condition(element)) {
240
+ return
241
+ }
242
+
243
+ let stopped = false
244
+
245
+ startWork(workTag)
246
+
247
+ let doStop = function() {
248
+ stopped = true
249
+ element.removeEventListener('load', doStop)
250
+ element.removeEventListener('error', doStop)
251
+ stopWork(workTag)
252
+ }
253
+
254
+ let checkCondition = function() {
255
+ if (stopped) {
256
+ // A `load` or `error` event has fired.
257
+ // We can stop here. No need to schedule another check.
258
+ return
259
+ } else if (isDetached(element) || !condition(element)) {
260
+ // If it is detached or if its `[src]` attribute changes to a data: URL
261
+ // we may never get a `load` or `error` event.
262
+ doStop()
263
+ } else {
264
+ scheduleCheckCondition()
265
+ }
266
+ }
267
+
268
+ let scheduleCheckCondition = function() {
269
+ setTimeout(checkCondition, 200)
270
+ }
271
+
272
+ element.addEventListener('load', doStop)
273
+ element.addEventListener('error', doStop)
274
+
275
+ // We periodically check whether we still think the element will
276
+ // produce a `load` or `error` event.
277
+ scheduleCheckCondition()
165
278
  }
166
279
 
167
280
  function onAnyElementChanged(changes) {
168
281
  changes.forEach(function(change) {
169
282
  change.addedNodes.forEach(function(addedNode) {
170
- if (isRemoteScript(addedNode)) {
171
- onRemoteScriptAdded(addedNode)
283
+ if (addedNode.nodeType === Node.ELEMENT_NODE) {
284
+ trackRemoteElement(addedNode, isRemoteScript, 'Script')
285
+ trackRemoteElement(addedNode, isRemoteImage, 'Image')
286
+ trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
172
287
  }
173
288
  })
174
289
  })
175
290
  }
176
291
 
292
+ function isDetached(element) {
293
+ return !document.contains(element)
294
+ }
295
+
177
296
  function whenReady(callback) {
178
297
  // Values are "loading", "interactive" and "completed".
179
298
  // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
@@ -188,7 +307,7 @@ window.CapybaraLockstep = (function() {
188
307
  trackFetch()
189
308
  trackXHR()
190
309
  trackInteraction()
191
- trackDynamicScripts()
310
+ trackRemoteElements()
192
311
  trackJQuery()
193
312
  trackHydration()
194
313
  }
@@ -203,11 +322,14 @@ window.CapybaraLockstep = (function() {
203
322
 
204
323
  return {
205
324
  track: track,
325
+ isBusy: isBusy,
326
+ isIdle: isIdle,
206
327
  startWork: startWork,
207
328
  stopWork: stopWork,
208
329
  synchronize: synchronize,
209
- isIdle: isIdle,
210
- isBusy: isBusy
330
+ reset: reset,
331
+ set debug(value) { debug = value },
332
+ set waitTasks(value) { waitTasks = value }
211
333
  }
212
334
  })()
213
335
 
@@ -17,7 +17,22 @@ 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
+ 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 = ''
26
+
27
+ if (debug = options.fetch(:debug, Lockstep.debug?))
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}"
33
+ end
34
+
35
+ js
21
36
  end
22
37
 
23
38
  end
@@ -23,14 +23,26 @@ module Capybara
23
23
  page.instance_variable_set(:@lockstep_synchronized, value)
24
24
  end
25
25
 
26
- def synchronize(lazy: false)
27
- if (lazy && synchronized?) || synchronizing? || disabled?
26
+ def synchronize(lazy: false, log: nil)
27
+ if (lazy && synchronized?) || synchronizing? || mode == :off
28
28
  return
29
29
  end
30
30
 
31
+ # Allow passing a log message that is only logged
32
+ # when we're actually synchronizing.
33
+ if log
34
+ self.log(log)
35
+ end
36
+
31
37
  synchronize_now
32
38
  end
33
39
 
40
+ def auto_synchronize(**options)
41
+ if mode == :auto
42
+ synchronize(**options)
43
+ end
44
+ end
45
+
34
46
  private
35
47
 
36
48
  def synchronize_now
@@ -39,6 +51,8 @@ module Capybara
39
51
 
40
52
  log 'Synchronizing'
41
53
 
54
+ start_time = current_seconds
55
+
42
56
  begin
43
57
  with_max_wait_time(timeout) do
44
58
  message_from_js = evaluate_async_script(<<~JS)
@@ -67,10 +81,16 @@ module Capybara
67
81
  log(message_from_js)
68
82
  else
69
83
  log message_from_js
70
- log "Synchronized successfully"
84
+ end_time = current_seconds
85
+ ms_elapsed = ((end_time.to_f - start_time) * 1000).round
86
+ log "Synchronized successfully [#{ms_elapsed} ms]"
71
87
  self.synchronized = true
72
88
  end
73
89
  end
90
+ rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
91
+ log "Could not synchronize within #{timeout} seconds"
92
+ # Don't raise an error, this may happen if the server is slow to respond.
93
+ # We will retry on the next Capybara synchronize call.
74
94
  rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
75
95
  log ERROR_ALERT_OPEN
76
96
  # Don't raise an error, this will happen in an innocent test.
@@ -120,6 +140,10 @@ module Capybara
120
140
  # no-op
121
141
  end
122
142
 
143
+ def current_seconds
144
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
145
+ end
146
+
123
147
  end
124
148
 
125
149
  end
@@ -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 = "1.0.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.3.3
4
+ version: 1.0.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-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby2_keywords
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description:
56
70
  email:
57
71
  - henning.koch@makandra.de
@@ -59,9 +73,11 @@ executables: []
59
73
  extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
76
+ - ".github/workflows/test.yml"
62
77
  - ".gitignore"
63
78
  - ".rspec"
64
79
  - ".ruby-version"
80
+ - CHANGELOG.md
65
81
  - Gemfile
66
82
  - Gemfile.lock
67
83
  - LICENSE.txt