capybara-lockstep 1.0.0 → 1.1.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: 86f6aba93856539de9e2df301e720cced0c444de2d62436e72ea620765f9b3e1
4
- data.tar.gz: 1ae5dc301b4761d7c51796313ea4a6210596b5975e02f639410916ddc53060b5
3
+ metadata.gz: 4ece5e0daeb6883b02791ed06f6ff2afca446e5aa5635f22d3ec5b94d0e96aa7
4
+ data.tar.gz: 5e4d53a8c59f71071aa48fee8ec87fa62e5c189e026b5cac563fd9427a082c59
5
5
  SHA512:
6
- metadata.gz: b1dc1f1e4a8af3d4d02f4d7b2aec3ae99844ba42bd3713028a138842bdc296c6bc0ed39688b8c15d7976830cc97df9ebc96cee3f3cfc9ab5f799e99ec59fee23
7
- data.tar.gz: 50e27f5304cc7fbc1ed0c74ee87311ef8c8a333ecd4395af7cf155305ce0607f4b0f20defdf901d8f5cb3cbe8fc1ddb23638d9587c27f15e2071aea59c24297c
6
+ metadata.gz: 7e68bf91ab8b5ecf91b145159697e7990dbbbb3a5497955d50d91ea83d802c50195f803ae1fd029b2b76dc18b407bd1995b03aa292e30873cd35f8746dfa509e
7
+ data.tar.gz: 8fb8d9b19fc47b5769208d36125b6cc2fb40375a8d5b46216ec318a3fd8459ca26f6bf22f352efcf350b7d7cbfe89f7bb866633fb4e096812b7d31513d4c7bae
@@ -28,6 +28,12 @@ jobs:
28
28
  BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
29
29
  steps:
30
30
  - uses: actions/checkout@v2
31
+ - name: Install Chrome
32
+ uses: browser-actions/setup-chrome@latest
33
+ - name: Show Chrome version
34
+ run: chrome --version
35
+ - name: Install ChromeDriver
36
+ uses: nanasess/setup-chromedriver@master
31
37
  - name: Install ruby
32
38
  uses: ruby/setup-ruby@v1
33
39
  with:
data/CHANGELOG.md CHANGED
@@ -2,32 +2,68 @@ All notable changes to this project will be documented in this file.
2
2
 
3
3
  This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## 1.1.0
5
6
 
6
- ## Unreleased
7
+ - Stop handling of `[data-initializing]` attribute. Apps that have late initialization after the `load` event can just use `CapybaraLockstep.startWork()`.
8
+ - Remove useless tracking of interaction events like `"click"` or `"focus"`. If such an event handler would start an AJAX request, it is already tracked.
9
+ - On apps with Unpoly 0.x, wait for one more task after `DOMContentLoaded`. Please upgrade to Unpoly 1.x or 2.x, as this logic will be removed in a year or so.
7
10
 
8
- ### Breaking changes
11
+ ## 1.0.0
9
12
 
10
- -
13
+ - First stable release.
14
+ - Replace option `Capybara::Lockstep.config` (`true`, `false`) with a more refined option `.mode` (`:auto`, `:manual`, `:off`)
11
15
 
12
- ### Compatible changes
16
+ ## 0.7.0
13
17
 
14
- -
18
+ - Ruby 3 compatibility.
19
+ - Fix logging.
15
20
 
16
- ## 0.7.0 - 2021-05-04
21
+ ## 0.6.0
17
22
 
18
- ### Compatible changes
23
+ - Synchronize around `evaluate_script` and `execute_script`.
24
+ - Improve logging.
19
25
 
20
- - add changelog
21
- - add gemika for tests with github actions
22
- - add Ruby 3 support
26
+ ## 0.5.0
23
27
 
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
28
+ - Allow developer to signal custom async work.
29
+ - Option to wait additional tasks, to handle legacy promise implementations.
30
+ - Debugging log can be enabled during a running test.
31
+ - Also wait for images and iframes.
32
+
33
+ ## 0.4.0
34
+
35
+ - Don't fail the test when synchronization times out.
36
+ - Capybara::Lockstep.debug = true will now also enable client-side logging on the browser's JavaScript console.
37
+ - Always wait at least for `Capybara.default_max_wait_time`.
38
+
39
+ ## 0.3.2
40
+
41
+ - Delay synchronization when an alert is open (instead of failing)
42
+
43
+
44
+ ## 0.3.1
45
+
46
+ - Fix typo in log message
47
+
48
+ ## 0.3.0
49
+
50
+ - Rework entire waiting logic to be lazy.
51
+ - There is now a single method `Capybara::Lockstep.synchronize` (no distinction between awaiting "initialization" and "idle").
52
+
53
+ ## 0.2.3
54
+
55
+ - When we cannot wait for browser idle due to an open alert, wait before the next Capybara synchronize
56
+
57
+ ## 0.2.2
58
+
59
+ - Fix incorrect data in gemspec.
60
+
61
+
62
+ ## 0.2.1
63
+
64
+ - Internal changes.
65
+
66
+
67
+ ## 0.2.0
68
+
69
+ - Initial release.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (1.0.0)
4
+ capybara-lockstep (1.1.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  ruby2_keywords
data/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # capybara-lockstep
2
2
 
3
- This Ruby gem synchronizes [Capybara](https://github.com/teamcapybara/capybara) commands with client-side JavaScript and AJAX requests. This greatly improves the stability of a full-stack integration test suite, even if that suite has timing issues.
3
+ This Ruby gem synchronizes [Capybara](https://github.com/teamcapybara/capybara) commands with client-side JavaScript and AJAX requests. This greatly improves the stability of an end-to-end ("E2E") test suite, even if that suite has timing issues.
4
+
5
+ The next section explain why your test suite is flaky and how capybara-lockstep can help.\
6
+ If you don't care you may skip to [installation instructions](#installation).
4
7
 
5
8
 
6
9
  Why are tests flaky?
7
10
  --------------------
8
11
 
9
- A naively written integration test will have [race conditions](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) between the test script and the controlled browser. How often these timing issues will fail your test depends on luck and your machine's performance. You may not see these issues for years until a colleague runs your suite on their new laptop.
12
+ A naively written E2E test will have [race conditions](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) between the test script and the controlled browser. How often these timing issues will fail your test depends on luck and your machine's performance. You may not see these issues for years until a colleague runs your suite on their new laptop.
10
13
 
11
14
  Here is a typical example for a test that will fail with unlucky timing:
12
15
 
@@ -23,28 +26,50 @@ end
23
26
 
24
27
  This test has four timing issues that may cause it to fail:
25
28
 
26
- 1. We click on the "New tweet" button, but the the JS event handler to open the tweet form wasn't registered yet.
29
+ 1. We click on the *New tweet* button, but the the JS event handler to open the tweet form wasn't registered yet.
27
30
  2. We start filling in the form, but it wasn't loaded yet.
28
31
  3. After sending the tweet we immediately navigate away, killing the form submission request that is still in flight. Hence the tweet will never appear in the next step.
29
32
  4. We look for the new tweet, but the timeline wasn't loaded yet.
30
33
 
31
- Capybara will retry individual commands or expectations when they fail. However, only issues **2** and **4** can be healed by retrying.
34
+ [Capybara will retry](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends) individual commands or expectations when they fail.\
35
+ However, only issues **2** and **4** can be healed by retrying.
36
+
37
+ While it is [possible](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) to remove most of the timing issues above, it requires skill and discipline.\
38
+ capybara-lockstep fixes issues **1**, **2**, **3** and **4** without any changes to the test code.
39
+
40
+
41
+ ### This is a JavaScript problem
42
+
43
+ The timing issues above will only manifest in an app where links, forms and buttons are handled by JavaScript.
44
+
45
+ When all you have is standard HTML links and forms, stock Capybara will not see timing issues:
46
+
47
+ - After a `visit()` Capybara/WebDriver will wait until the page is completely loaded
48
+ - When following a link Capybara/WebDriver will wait until the link destination is completely loaded
49
+ - When submitting a form Capybara/WebDriver will wait until the response is completely loaded
50
+
51
+ However, when JavaScript handles a link click, you get **zero guarantees**.\
52
+ Capybara/WebDriver **will not wait** for AJAX requests or any other async work.
32
53
 
33
- While it is [possible](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) to remove most of the timing issues above, it requires skill and discipline. capybara-lockstep fixes issues **1**, **2**, **3** and **4** without any changes to the test code.
34
54
 
35
55
 
36
56
  How capybara-lockstep helps
37
57
  ---------------------------
38
58
 
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.
59
+ 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](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends).
40
60
 
41
- Whenever Capybara visits a new URL or simulates a user interaction (clicking, typing, etc.):
61
+ Before Capybara simulates a user interaction (clicking, typing, etc.) or before it visits a new URL:
42
62
 
43
- - capybara-lockstep waits for all document resources to load.
63
+ - capybara-lockstep waits for all document resources to load (images, CSS, fonts, frames).
64
+ - capybara-lockstep waits for any AJAX requests to finish.
44
65
  - capybara-lockstep waits for client-side JavaScript to render or hydrate DOM elements.
45
- - capybara-lockstep waits for any AJAX requests.
46
66
  - 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.
67
+ - capybara-lockstep waits for dynamically inserted `<img>` or `<iframe>` elements to load.
68
+
69
+ In summary Capybara can no longer observe the page while HTTP requests are in flight.
70
+ This covers most async work that causes flaky tests.
71
+
72
+ You can also configure capybara-lockstep to [wait for other async work](#signaling-asynchronous-work) that does not involve the network, like animations.
48
73
 
49
74
 
50
75
  Installation
@@ -76,127 +101,82 @@ And then execute:
76
101
  $ bundle install
77
102
  ```
78
103
 
79
- If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
104
+ If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec), `test_helper.rb` (Minitest) or `env.rb` (Cucumber).
80
105
 
81
106
 
82
107
  ### Including the JavaScript snippet
83
108
 
84
109
  capybara-lockstep requires a JavaScript snippet to be embedded by the application under test. If that snippet is missing on a screen, capybara-lockstep will not be able to synchronize with the browser. In that case the test will continue without synchronization.
85
110
 
86
- If you're using Rails you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
111
+ **If you're using Rails** you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
87
112
 
88
113
  ```erb
89
114
  <%= capybara_lockstep if Rails.env.test? %>
90
115
  ```
91
116
 
92
- Ideally the snippet should be included in the `<head>` before any other `<script>` tags. If that's impractical you will also see some benefit if you insert it later.
117
+ Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
93
118
 
94
- If you have a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), the `capybara_lockstep` helper will insert a CSP nonce by default. You can also pass a `:nonce` option.
119
+ **If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
95
120
 
96
- If you're not using Rails you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
121
+ **If you have a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)** the `capybara_lockstep` Rails helper will insert a CSP nonce by default. You can also pass an explicit nonce string using the `:nonce` option.
97
122
 
98
123
 
99
- ### Signaling the end of page initialization
100
124
 
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.
125
+ ### Verify successful integration
102
126
 
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.
127
+ capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
104
128
 
105
- To signal that JavaScript is still initializing, your application layouts should render the `<body>` element with an `[data-initializing]` attribute:
129
+ Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate the [debugging log](#debugging-log) before your test:
106
130
 
107
- ```html
108
- <body data-initializing>
131
+ ```ruby
132
+ Capybara::Lockstep.debug = true
109
133
  ```
110
134
 
111
- Your application JavaScript should remove the `[data-initializing]` attribute when it is done rendering the initial page.
135
+ You should see messages like this in your console:
112
136
 
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.
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.**
137
+ ```text
138
+ [capybara-lockstep] Synchronizing
139
+ [capybara-lockstep] Finished waiting for JavaScript
140
+ [capybara-lockstep] Synchronized successfully
141
+ ```
116
142
 
117
- #### Example: Vanilla JS
143
+ Note that you may see some failures from tests with wrong assertions, which previously passed due to lucky timing.
118
144
 
119
- If all your initializing JavaScript runs synchronously on `DOMContentLoaded`, you can remove `[data-initializing]` in an event handler:
120
145
 
121
- ```js
122
- document.addEventListener('DOMContentLoaded', function() {
123
- // Initialize the page here
124
- document.body.removeAttribute('data-initializing')
125
- })
126
- ```
127
146
 
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:
147
+ ## Signaling asynchronous work
129
148
 
130
- ```js
131
- document.addEventListener('DOMContentLoaded', function() {
132
- Libary.initialize({
133
- onFinished: function() {
134
- document.body.removeAttribute('data-initializing')
135
- }
136
- })
137
- })
138
- ```
149
+ By default capybara-lockstep blocks all async work that
139
150
 
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]`:
151
+ If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
141
152
 
142
153
  ```js
143
- document.addEventListener('DOMContentLoaded', function() {
144
- Libary.initialize()
145
- setTimeout(function() { document.body.removeAttribute('data-initializing') })
154
+ CapybaraLockstep.startWork('Eject warp core')
155
+ doAsynchronousWork().then(function() {
156
+ CapybaraLockstep.stopWork('Eject warp core')
146
157
  })
147
158
  ```
148
159
 
149
- If your initialization code lazy-loads another script, you should only remove `[data-initializing]` once that is done:
160
+ 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:
150
161
 
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
- })
162
+ ```text
163
+ [capybara-lockstep] Started work: Eject warp core [1 jobs]
164
+ [capybara-lockstep] Finished work: Eject warp core [0 jobs]
158
165
  ```
159
166
 
167
+ You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
160
168
 
161
- #### Example: Unpoly
162
-
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]`:
164
-
165
- ```js
166
- up.compiler('body', function(body) {
167
- body.removeAttribute('data-initializing')
168
- })
169
- ```
170
169
 
171
- #### Example: AngularJS 1
170
+ ## Note on interacting with the JavaScript API
172
171
 
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]`:
172
+ 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.
174
173
 
175
174
  ```js
176
- app.directive('body', function() {
177
- return {
178
- restrict: 'E',
179
- link: function() {
180
- document.body.removeAttribute('data-initializing')
181
- }
182
- }
183
- })
184
- ```
185
-
186
- ### Verify successful integration
187
-
188
- capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
189
-
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:
191
-
192
- ```text
193
- [capybara-lockstep] Synchronizing
194
- [capybara-lockstep] Finished waiting for JavaScript
195
- [capybara-lockstep] Synchronized successfully
175
+ if (window.CapybaraLockstep) {
176
+ // interact with CapybaraLockstep
177
+ }
196
178
  ```
197
179
 
198
- Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
199
-
200
180
 
201
181
  ## Performance impact
202
182
 
@@ -204,7 +184,7 @@ capybara-lockstep may or may not impact the runtime of your test suite. It depen
204
184
 
205
185
  While waiting for the browser to be idle does take a few milliseconds, Capybara no longer needs to retry failed commands. You will also save time from not needing to re-run failed tests.
206
186
 
207
- In casual testing I experienced a performance impact between +/- 10%.
187
+ In casual testing with large test suites I experienced a performance impact between +/- 10%.
208
188
 
209
189
 
210
190
  ## Debugging log
@@ -243,9 +223,9 @@ Capybara::Lockstep.debug = Rails.logger
243
223
 
244
224
  ### Logging in the browser only
245
225
 
246
- To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
226
+ To enable logging in the browser console (but not STDOUT), include the [JavaScript snippet](#including-the-javascript-snippet) with `{ debug: true }`:
247
227
 
248
- ```
228
+ ```ruby
249
229
  capybara_lockstep(debug: true)
250
230
  ```
251
231
 
@@ -253,7 +233,11 @@ capybara_lockstep(debug: true)
253
233
 
254
234
  By default capybara-lockstep will wait `Capybara.default_max_wait_time` seconds for the page initialize and for JavaScript and AJAX request to finish.
255
235
 
256
- When synchronization times out, capybara-lockstep will log but not raise an error.
236
+ When synchronization times out, capybara-lockstep will [log](#debugging-log):
237
+
238
+ ```text
239
+ [capybara-lockstep] Could not synchronize within 3 seconds
240
+ ```
257
241
 
258
242
  You can configure a different timeout:
259
243
 
@@ -261,10 +245,19 @@ You can configure a different timeout:
261
245
  Capybara::Lockstep.timeout = 5 # seconds
262
246
  ```
263
247
 
264
- To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
248
+ By default Capybara will **not** raise an error after a timeout. You may occasionally get a slow server response, and Capybara will retry synchronization before the next interaction or `visit`. This is often good enough.
249
+
250
+ If you want to be strict you may configure that an `Capybara::Lockstep::Timeout` error is raised after a timeout:
251
+
252
+ ```ruby
253
+ Capybara::Lockstep.timeout_with = :error
254
+ ```
255
+
256
+ To revert to defaults:
265
257
 
266
258
  ```ruby
267
259
  Capybara::Lockstep.timeout = nil
260
+ Capybara::Lockstep.timeout_with = nil
268
261
  ```
269
262
 
270
263
 
@@ -300,7 +293,7 @@ ensure
300
293
  end
301
294
  ```
302
295
 
303
- Note that you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
296
+ In the `:manual` mode you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
304
297
 
305
298
  To completely disable synchronization:
306
299
 
@@ -310,36 +303,6 @@ Capybara::Lockstep.synchronize # will not synchronize
310
303
  ```
311
304
 
312
305
 
313
- ## Signaling asynchronous work
314
-
315
- If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
316
-
317
- ```js
318
- CapybaraLockstep.startWork('Eject warp core')
319
- doAsynchronousWork().then(function() {
320
- CapybaraLockstep.stopWork('Eject warp core')
321
- })
322
- ```
323
-
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.
332
-
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.
337
-
338
- ```js
339
- if (window.CapybaraLockstep) {
340
- // interact with CapybaraLockstep
341
- }
342
- ```
343
306
 
344
307
  ## Handling legacy promises
345
308
 
@@ -364,16 +327,31 @@ If you see longer `then()` chains in your code, you may need to configure a high
364
327
  This will have a negative performance impact on your test suite.
365
328
 
366
329
 
367
- ## Development
330
+ ## Contributing
368
331
 
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.
332
+ Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
370
333
 
371
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
334
+ After checking out the repo, run `bin/setup` to install dependencies.
372
335
 
336
+ Then, run `rake spec` to run the tests.
373
337
 
374
- ## Contributing
338
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
375
339
 
376
- Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
340
+ ### Manually testing a change
341
+
342
+ To test an unrelased change with a test suite, we recommend to temporarily link the local repository from your test suites's `Gemfile`:
343
+
344
+ ```ruby
345
+ gem 'capybara-lockstep', path: '../capybara-lockstep'
346
+ ```
347
+
348
+ As an alternative you may also install this gem onto your local machine by running `bundle exec rake install`.
349
+
350
+ ### Releasing a new version
351
+
352
+ - Update the version number in `version.rb`
353
+ - Run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
354
+ - If RubyGems publishing seems to freeze, try entering your OTP code.
377
355
 
378
356
 
379
357
  ## License
@@ -10,6 +10,14 @@ module Capybara
10
10
  @timeout = seconds
11
11
  end
12
12
 
13
+ def timeout_with
14
+ @timeout_with.nil? ? :log : @timeout_with
15
+ end
16
+
17
+ def timeout_with=(action)
18
+ @timeout_with = action&.to_sym
19
+ end
20
+
13
21
  def debug?
14
22
  # @debug may also be a Logger object, so convert it to a boolean
15
23
  @debug.nil? ? false : !!@debug
@@ -38,7 +46,7 @@ module Capybara
38
46
  end
39
47
 
40
48
  def mode=(mode)
41
- @mode = mode
49
+ @mode = mode&.to_sym
42
50
  end
43
51
 
44
52
  def enabled=(enabled)
@@ -1,6 +1,6 @@
1
1
  module Capybara
2
2
  module Lockstep
3
3
  class Error < StandardError; end
4
- class Busy < Error; end
4
+ class Timeout < Error; end
5
5
  end
6
6
  end
@@ -123,43 +123,21 @@ window.CapybaraLockstep = (function() {
123
123
  }
124
124
  }
125
125
 
126
- function trackInteraction() {
127
- // We already override all interaction methods in the Selenium browser nodes, so they
128
- // wait for an idle frame afterwards. However a test script might also dispatch synthetic
129
- // events with executate_script() to manipulate the browser in ways that are not possible
130
- // with the Capybara API. When we observe such an event we wait until the end of the microtask,
131
- // assuming any busy action will be queued by then.
132
- ['click', 'mousedown', 'keydown', 'change', 'input', 'submit', 'focusin', 'focusout', 'scroll'].forEach(function(eventType) {
133
- // Use { useCapture: true } so we get the event before another listener
134
- // can prevent it from bubbling up to the document.
135
- document.addEventListener(eventType, onInteraction, { capture: true, passive: true })
136
- })
137
- }
138
-
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)
147
- }
148
-
149
126
  function trackRemoteElements() {
150
127
  if (!window.MutationObserver) {
151
128
  return
152
129
  }
153
130
 
154
- // Dynamic imports or analytics snippets may insert a <script src>
155
- // tag that loads and executes additional JavaScript. We want to be isBusy()
131
+ // Dynamic imports or analytics snippets may insert a script element
132
+ // that loads and executes additional JavaScript. We want to be isBusy()
156
133
  // until such scripts have loaded or errored.
157
134
  let observer = new MutationObserver(onAnyElementChanged)
158
135
  observer.observe(document, { subtree: true, childList: true })
159
136
  }
160
137
 
161
138
  function trackJQuery() {
162
- // jQuery may be loaded after us, so we wait until DOMContentReady.
139
+ // CapybaraLockstep.track() is called as the first script in the head.
140
+ // jQuery will be loaded after us, so we wait until DOMContentReady.
163
141
  whenReady(function() {
164
142
  if (!window.jQuery || waitTasks > 0) {
165
143
  return
@@ -180,28 +158,6 @@ window.CapybaraLockstep = (function() {
180
158
  })
181
159
  }
182
160
 
183
- let INITIALIZING_ATTRIBUTE = 'data-initializing'
184
-
185
- function trackHydration() {
186
- // Until we have a body on which we can observe [data-initializing]
187
- // we consider ourselves busy.
188
- startWork()
189
- whenReady(function() {
190
- stopWorkNow()
191
- if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
192
- startWork('Page initialization')
193
- let observer = new MutationObserver(onInitializingAttributeChanged)
194
- observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
195
- }
196
- })
197
- }
198
-
199
- function onInitializingAttributeChanged() {
200
- if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
201
- stopWork('Page initialization')
202
- }
203
- }
204
-
205
161
  function isRemoteScript(element) {
206
162
  if (element.tagName === 'SCRIPT') {
207
163
  let src = element.getAttribute('src')
@@ -303,13 +259,28 @@ window.CapybaraLockstep = (function() {
303
259
  }
304
260
  }
305
261
 
262
+ function trackOldUnpoly() {
263
+ // CapybaraLockstep.track() is called as the first script in the head.
264
+ // Unpoly will be loaded after us, so we wait until DOMContentReady.
265
+ whenReady(function() {
266
+ // Unpoly 0.x would wait one task after DOMContentLoaded before booting.
267
+ // There's a slim chance that Capybara can observe the page before compilers have run.
268
+ // Unpoly 1.0+ runs compilers on DOMContentLoaded, so there's no issue.
269
+ if (window.up?.version?.startsWith('0.')) {
270
+ startWork('Old Unpoly')
271
+ setTimeout(function () {
272
+ stopWork('Old Unpoly')
273
+ })
274
+ }
275
+ })
276
+ }
277
+
306
278
  function track() {
279
+ trackOldUnpoly()
307
280
  trackFetch()
308
281
  trackXHR()
309
- trackInteraction()
310
282
  trackRemoteElements()
311
283
  trackJQuery()
312
- trackHydration()
313
284
  }
314
285
 
315
286
  function synchronize(callback) {
@@ -68,6 +68,8 @@ module Capybara
68
68
  if (protocol === 'data:' || protocol == 'about:') {
69
69
  done(#{ERROR_PAGE_MISSING.to_json})
70
70
  } else if (document.readyState === 'complete') {
71
+ // WebDriver always waits for the `load` event after a visit(),
72
+ // unless a different page load strategy was configured.
71
73
  synchronize()
72
74
  } else {
73
75
  window.addEventListener('load', synchronize)
@@ -88,9 +90,14 @@ module Capybara
88
90
  end
89
91
  end
90
92
  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.
93
+ timeout_message = "Could not synchronize within #{timeout} seconds"
94
+ log timeout_message
95
+ if timeout_with == :error
96
+ raise Timeout, timeout_message
97
+ else
98
+ # Don't raise an error, this may happen if the server is slow to respond.
99
+ # We will retry on the next Capybara synchronize call.
100
+ end
94
101
  rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
95
102
  log ERROR_ALERT_OPEN
96
103
  # Don't raise an error, this will happen in an innocent test.
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.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: 1.0.0
4
+ version: 1.1.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-06-24 00:00:00.000000000 Z
11
+ date: 2021-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara