capybara-lockstep 0.3.3 → 1.0.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 +44 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -8
- data/README.md +120 -57
- data/Rakefile +8 -0
- data/capybara-lockstep.gemspec +1 -0
- data/lib/capybara-lockstep/capybara_ext.rb +64 -9
- data/lib/capybara-lockstep/configuration.rb +60 -10
- data/lib/capybara-lockstep/helper.js +180 -58
- data/lib/capybara-lockstep/helper.rb +16 -1
- data/lib/capybara-lockstep/lockstep.rb +27 -3
- data/lib/capybara-lockstep/logging.rb +7 -1
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86f6aba93856539de9e2df301e720cced0c444de2d62436e72ea620765f9b3e1
|
|
4
|
+
data.tar.gz: 1ae5dc301b4761d7c51796313ea4a6210596b5975e02f639410916ddc53060b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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 (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
|
-
|
|
36
|
+
eventmachine (1.2.7)
|
|
37
|
+
gemika (0.6.0)
|
|
38
|
+
i18n (1.8.10)
|
|
33
39
|
concurrent-ruby (~> 1.0)
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
@@ -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
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
+
To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
|
|
240
265
|
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
257
|
-
|
|
258
|
-
Capybara::Lockstep.synchronize
|
|
259
|
-
end
|
|
283
|
+
```js
|
|
284
|
+
CapybaraLockstep.synchronize(callback)
|
|
260
285
|
```
|
|
261
286
|
|
|
262
|
-
## JavaScript API
|
|
263
287
|
|
|
264
|
-
|
|
288
|
+
## Disabling synchronization
|
|
265
289
|
|
|
266
|
-
|
|
290
|
+
Sometimes you want to disable browser synchronization, e.g. to observe a loading spinner during a long-running request.
|
|
267
291
|
|
|
268
|
-
|
|
292
|
+
To disable automatic synchronization:
|
|
269
293
|
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
293
|
-
|
|
339
|
+
if (window.CapybaraLockstep) {
|
|
340
|
+
// interact with CapybaraLockstep
|
|
341
|
+
}
|
|
294
342
|
```
|
|
295
343
|
|
|
296
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/capybara-lockstep.gemspec
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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=(
|
|
18
|
-
@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
|
|
32
|
+
def mode
|
|
22
33
|
if javascript_driver?
|
|
23
|
-
@
|
|
34
|
+
@mode.nil? ? :auto : @mode
|
|
24
35
|
else
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
|
20
|
+
return jobCount === 0
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
function isBusy() {
|
|
13
24
|
return !isIdle()
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
function
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
36
|
-
|
|
52
|
+
function startWorkUntil(promise, tag) {
|
|
53
|
+
startWork(tag)
|
|
54
|
+
let taggedStopWork = stopWork.bind(this, tag)
|
|
55
|
+
promise.then(taggedStopWork, taggedStopWork)
|
|
56
|
+
}
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
91
|
+
let oldFetch = window.fetch
|
|
52
92
|
window.fetch = function() {
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
+
stopWorkNow()
|
|
137
191
|
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
|
138
|
-
startWork()
|
|
139
|
-
|
|
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(
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
210
|
+
return src && (!type || /javascript/i.test(type))
|
|
157
211
|
}
|
|
158
212
|
}
|
|
159
213
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 (
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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? ||
|
|
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
|
-
|
|
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
|
|
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
|
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
|
+
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-
|
|
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
|