capybara-lockstep 0.3.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +36 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +34 -7
- data/README.md +109 -56
- data/Rakefile +8 -0
- data/capybara-lockstep.gemspec +1 -0
- data/lib/capybara-lockstep/capybara_ext.rb +62 -7
- data/lib/capybara-lockstep/configuration.rb +44 -4
- data/lib/capybara-lockstep/helper.js +180 -72
- data/lib/capybara-lockstep/helper.rb +16 -1
- data/lib/capybara-lockstep/lockstep.rb +46 -16
- data/lib/capybara-lockstep/logging.rb +8 -2
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8335399770451283be7443be41f3f1e5c2b0401f16a64a7722fb6b39ec2f3143
|
4
|
+
data.tar.gz: 601249c76c50789b44199e734a64fb039cd3b77c1db3e9f9068b1e7dea188216
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 638b0671d9f919c85b972cb2af4df763932a8c00ab21cd8387e3b483fda43448bb3c28b434aa821c0c6e75d4c67d82acb1dad0788128a5b1761bae0ae45400d6
|
7
|
+
data.tar.gz: eed68edd632b78dea169e208162e4a041cfc1ed2a78c6593eebf55d27d2fe54d9161e1c65f21771cb66de2ef4e2e2ce92573569118e30e8a3148368ffaaafe53
|
@@ -0,0 +1,36 @@
|
|
1
|
+
---
|
2
|
+
name: Tests
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
pull_request:
|
8
|
+
branches:
|
9
|
+
- master
|
10
|
+
jobs:
|
11
|
+
test:
|
12
|
+
runs-on: ubuntu-20.04
|
13
|
+
strategy:
|
14
|
+
fail-fast: false
|
15
|
+
matrix:
|
16
|
+
include:
|
17
|
+
- ruby: 2.6.6
|
18
|
+
gemfile: Gemfile
|
19
|
+
- ruby: 2.7.2
|
20
|
+
gemfile: Gemfile
|
21
|
+
- ruby: 3.0.1
|
22
|
+
gemfile: Gemfile
|
23
|
+
env:
|
24
|
+
BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
|
25
|
+
steps:
|
26
|
+
- uses: actions/checkout@v2
|
27
|
+
- name: Install ruby
|
28
|
+
uses: ruby/setup-ruby@v1
|
29
|
+
with:
|
30
|
+
ruby-version: "${{ matrix.ruby }}"
|
31
|
+
- name: Bundle
|
32
|
+
run: |
|
33
|
+
gem install bundler:2.2.15
|
34
|
+
bundle install --no-deployment
|
35
|
+
- name: Run tests
|
36
|
+
run: bundle exec rake spec
|
data/.gitignore
CHANGED
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
data/Gemfile.lock
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
capybara-lockstep (0.
|
4
|
+
capybara-lockstep (0.7.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)
|
@@ -17,6 +18,7 @@ GEM
|
|
17
18
|
zeitwerk (~> 2.3)
|
18
19
|
addressable (2.7.0)
|
19
20
|
public_suffix (>= 2.0.2, < 5.0)
|
21
|
+
byebug (11.1.3)
|
20
22
|
capybara (3.35.3)
|
21
23
|
addressable
|
22
24
|
mini_mime (>= 0.1.3)
|
@@ -26,16 +28,28 @@ GEM
|
|
26
28
|
regexp_parser (>= 1.5, < 3.0)
|
27
29
|
xpath (~> 3.2)
|
28
30
|
childprocess (3.0.0)
|
31
|
+
chrome_remote (0.3.0)
|
32
|
+
websocket-driver (~> 0.6)
|
29
33
|
concurrent-ruby (1.1.8)
|
34
|
+
daemons (1.3.1)
|
30
35
|
diff-lcs (1.3)
|
31
|
-
|
36
|
+
eventmachine (1.2.7)
|
37
|
+
gemika (0.6.0)
|
38
|
+
i18n (1.8.10)
|
32
39
|
concurrent-ruby (~> 1.0)
|
33
|
-
|
34
|
-
|
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)
|
47
|
+
mini_portile2 (2.5.1)
|
35
48
|
minitest (5.14.4)
|
36
|
-
nokogiri (1.11.
|
49
|
+
nokogiri (1.11.3)
|
37
50
|
mini_portile2 (~> 2.5.0)
|
38
51
|
racc (~> 1.4)
|
52
|
+
phantomjs (2.1.1.0)
|
39
53
|
public_suffix (4.0.6)
|
40
54
|
racc (1.5.2)
|
41
55
|
rack (2.2.3)
|
@@ -56,12 +70,20 @@ GEM
|
|
56
70
|
diff-lcs (>= 1.2.0, < 2.0)
|
57
71
|
rspec-support (~> 3.7.0)
|
58
72
|
rspec-support (3.7.0)
|
73
|
+
ruby2_keywords (0.0.4)
|
59
74
|
rubyzip (2.3.0)
|
60
75
|
selenium-webdriver (3.142.7)
|
61
76
|
childprocess (>= 0.5, < 4.0)
|
62
77
|
rubyzip (>= 1.2.2)
|
78
|
+
thin (1.8.0)
|
79
|
+
daemons (~> 1.0, >= 1.0.9)
|
80
|
+
eventmachine (~> 1.0, >= 1.0.4)
|
81
|
+
rack (>= 1, < 3)
|
63
82
|
tzinfo (2.0.4)
|
64
83
|
concurrent-ruby (~> 1.0)
|
84
|
+
websocket-driver (0.7.3)
|
85
|
+
websocket-extensions (>= 0.1.0)
|
86
|
+
websocket-extensions (0.1.5)
|
65
87
|
xpath (3.2.0)
|
66
88
|
nokogiri (~> 1.8)
|
67
89
|
zeitwerk (2.4.2)
|
@@ -70,9 +92,14 @@ PLATFORMS
|
|
70
92
|
ruby
|
71
93
|
|
72
94
|
DEPENDENCIES
|
95
|
+
byebug
|
73
96
|
capybara-lockstep!
|
97
|
+
chrome_remote
|
98
|
+
gemika
|
99
|
+
jasmine
|
74
100
|
rake (~> 13.0)
|
75
101
|
rspec (~> 3.0)
|
102
|
+
thin
|
76
103
|
|
77
104
|
BUNDLED WITH
|
78
|
-
2.2.
|
105
|
+
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
|
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
|
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
|
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
|
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
|
-
|
133
|
-
|
134
|
-
|
132
|
+
Libary.initialize({
|
133
|
+
onFinished: function() {
|
134
|
+
document.body.removeAttribute('data-initializing')
|
135
|
+
}
|
135
136
|
})
|
136
137
|
})
|
137
138
|
```
|
138
139
|
|
139
|
-
|
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.
|
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
|
@@ -173,9 +190,9 @@ capybara-lockstep will automatically patch Capybara to wait for the browser afte
|
|
173
190
|
Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber). You should see messages like this in your console:
|
174
191
|
|
175
192
|
```text
|
176
|
-
[
|
177
|
-
[
|
178
|
-
[
|
193
|
+
[capybara-lockstep] Synchronizing
|
194
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
195
|
+
[capybara-lockstep] Synchronized successfully
|
179
196
|
```
|
180
197
|
|
181
198
|
Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
|
@@ -192,30 +209,52 @@ In casual testing I experienced a performance impact between +/- 10%.
|
|
192
209
|
|
193
210
|
## Debugging log
|
194
211
|
|
195
|
-
capybara-lockstep
|
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
|
220
|
+
You should now see messages like this on your standard output:
|
202
221
|
|
203
222
|
```
|
204
|
-
[
|
205
|
-
[
|
206
|
-
[
|
223
|
+
[capybara-lockstep] Synchronizing
|
224
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
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
|
245
|
+
|
246
|
+
To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
|
247
|
+
|
248
|
+
```
|
249
|
+
capybara_lockstep(debug: true)
|
250
|
+
```
|
251
|
+
|
215
252
|
|
216
253
|
## Disabling synchronization
|
217
254
|
|
218
|
-
|
255
|
+
Sometimes you want to disable browser synchronization, e.g. to observe a loading spinner during a long-running request.
|
256
|
+
|
257
|
+
To disable synchronization:
|
219
258
|
|
220
259
|
```ruby
|
221
260
|
begin
|
@@ -226,9 +265,11 @@ ensure
|
|
226
265
|
end
|
227
266
|
```
|
228
267
|
|
229
|
-
##
|
268
|
+
## Synchronization timeout
|
269
|
+
|
270
|
+
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
271
|
|
231
|
-
|
272
|
+
When synchronization times out, capybara-lockstep will log but not raise an error.
|
232
273
|
|
233
274
|
You can configure a different timeout:
|
234
275
|
|
@@ -236,70 +277,82 @@ You can configure a different timeout:
|
|
236
277
|
Capybara::Lockstep.timeout = 5 # seconds
|
237
278
|
```
|
238
279
|
|
239
|
-
|
280
|
+
To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
|
240
281
|
|
241
|
-
|
282
|
+
```ruby
|
283
|
+
Capybara::Lockstep.timeout = nil
|
284
|
+
```
|
242
285
|
|
243
|
-
For additional edge cases you may interact with capybara-lockstep from your Ruby code.
|
244
286
|
|
287
|
+
## Manual synchronization
|
245
288
|
|
246
|
-
|
289
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
|
247
290
|
|
248
|
-
|
291
|
+
For additional edge cases you may manually tell capybara-lockstep to wait. The following Ruby method will block until the browser is idle:
|
249
292
|
|
250
293
|
```ruby
|
251
294
|
Capybara::Lockstep.synchronize
|
252
295
|
```
|
253
296
|
|
254
|
-
|
297
|
+
You may also synchronize from your client-side JavaScript. The following will run the given callback once the browser is idle:
|
255
298
|
|
256
|
-
```
|
257
|
-
|
258
|
-
|
259
|
-
|
299
|
+
```js
|
300
|
+
CapybaraLockstep.synchronize(callback)
|
301
|
+
```
|
302
|
+
|
303
|
+
## Signaling asynchronous work
|
304
|
+
|
305
|
+
If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
|
306
|
+
|
307
|
+
```js
|
308
|
+
CapybaraLockstep.startWork('Eject warp core')
|
309
|
+
doAsynchronousWork().then(function() {
|
310
|
+
CapybaraLockstep.stopWork('Eject warp core')
|
311
|
+
})
|
312
|
+
```
|
313
|
+
|
314
|
+
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:
|
315
|
+
|
316
|
+
```text
|
317
|
+
[capybara-lockstep] Started work: Eject warp core [1 jobs]
|
318
|
+
[capybara-lockstep] Finished work: Eject warp core [0 jobs]
|
260
319
|
```
|
261
320
|
|
262
|
-
|
321
|
+
You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
|
263
322
|
|
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
323
|
|
266
|
-
|
324
|
+
## Note on interacting with the JavaScript API
|
267
325
|
|
268
|
-
|
326
|
+
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.
|
269
327
|
|
270
328
|
```js
|
271
329
|
if (window.CapybaraLockstep) {
|
272
|
-
CapybaraLockstep
|
330
|
+
// interact with CapybaraLockstep
|
273
331
|
}
|
274
332
|
```
|
275
333
|
|
276
|
-
|
334
|
+
## Handling legacy promises
|
277
335
|
|
278
|
-
|
336
|
+
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.
|
337
|
+
|
338
|
+
This means there is a time window in which all AJAX requests have finished, but their callbacks have not yet run:
|
279
339
|
|
280
340
|
```js
|
281
|
-
|
282
|
-
|
283
|
-
CapybaraLockstep.stopWork()
|
341
|
+
$.ajax('/foo').then(function() {
|
342
|
+
// This callback runs one task after the response was received
|
284
343
|
})
|
285
344
|
```
|
286
345
|
|
287
|
-
|
288
|
-
|
289
|
-
You can query capybara-lockstep whether it considers the browser to be busy or idle:
|
346
|
+
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:
|
290
347
|
|
291
348
|
```js
|
292
|
-
|
293
|
-
CapybaraLockstep.isIdle() // => true
|
349
|
+
Capybara:Lockstep.wait_tasks = 1
|
294
350
|
```
|
295
351
|
|
296
|
-
|
352
|
+
If you see longer `then()` chains in your code, you may need to configure a higher number of tasks to wait.
|
297
353
|
|
298
|
-
This will
|
354
|
+
This will have a negative performance impact on your test suite.
|
299
355
|
|
300
|
-
```js
|
301
|
-
CapybaraLockstep.synchronize(callback)
|
302
|
-
```
|
303
356
|
|
304
357
|
## Development
|
305
358
|
|