capybara-lockstep 1.0.0 → 1.2.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: ec0d3874212ec29a338fa16ef831e76c0b8dc65b4a200f3a305b7ce41bd76957
4
+ data.tar.gz: b5d48346fb7224b2154512534757ef96638f1df9cc62f9d0f911aac8d6a5f846
5
5
  SHA512:
6
- metadata.gz: b1dc1f1e4a8af3d4d02f4d7b2aec3ae99844ba42bd3713028a138842bdc296c6bc0ed39688b8c15d7976830cc97df9ebc96cee3f3cfc9ab5f799e99ec59fee23
7
- data.tar.gz: 50e27f5304cc7fbc1ed0c74ee87311ef8c8a333ecd4395af7cf155305ce0607f4b0f20defdf901d8f5cb3cbe8fc1ddb23638d9587c27f15e2071aea59c24297c
6
+ metadata.gz: 4d44c17fa50ca0fd386b34ecea302148989f4d48d63c48820bcfff75bcd51c63cfbacea0053e63fc1c27f6347fbe252f14be83372aed123a6a1c2574328fe42e
7
+ data.tar.gz: 9e7f5cfcbb6b3750277bc55c37484e17859aa4d96f1b8f7b6c1865f4467ae00fad5311e11ea0bfc73d5b8ec5c925df5c8ec6bb2473da6b431296736c89972794
@@ -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,105 @@ 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.2.0 - 2022-09-12
5
6
 
6
- ## Unreleased
7
+ ### Synchronization around history navigation
7
8
 
8
- ### Breaking changes
9
+ We now synchronize before and after history navigation using the following Capybara methods:
9
10
 
10
- -
11
+ - `page.refresh`
12
+ - `page.go_back`
13
+ - `page.go_forward`
11
14
 
12
- ### Compatible changes
15
+ We also synchronize before `current_url` in case running a JavaScript task wants to update the URL when done.
13
16
 
14
- -
17
+ ### Support for tests with multiple tabs or frames
15
18
 
16
- ## 0.7.0 - 2021-05-04
19
+ capybara-lockstep now supports test that work with [multiple frames](https://makandracards.com/makandra/34015-use-capybara-commands-inside-an-iframe) or [multiple tabs or windows](https://github.com/teamcapybara/capybara#working-with-windows).
20
+ We now synchronize before and after the following Capybara methods:
17
21
 
18
- ### Compatible changes
22
+ - `switch_to_frame`
23
+ - `within_frame`
24
+ - `switch_to_window`
25
+ - `within_window`
19
26
 
20
- - add changelog
21
- - add gemika for tests with github actions
22
- - add Ruby 3 support
27
+ ### Improved logging
23
28
 
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
29
+ - Only log when we're actually synchronizing
30
+ - Log the reason why we're synchronizing (e.g. before node access)
31
+ - Log which browser work we're waiting for (e.g. XHR request, image load)
32
+
33
+ ### Various changes
34
+
35
+ - Synchronize before accessing `page.html`.
36
+
37
+
38
+ ## 1.1.1 - 2022-03-16
39
+
40
+ - Activate rubygems MFA
41
+
42
+ ## 1.1.0
43
+
44
+ - Stop handling of `[data-initializing]` attribute. Apps that have late initialization after the `load` event can just use `CapybaraLockstep.startWork()`.
45
+ - Remove useless tracking of interaction events like `"click"` or `"focus"`. If such an event handler would start an AJAX request, it is already tracked.
46
+ - 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.
47
+
48
+ ## 1.0.0
49
+
50
+ - First stable release.
51
+ - Replace option `Capybara::Lockstep.config` (`true`, `false`) with a more refined option `.mode` (`:auto`, `:manual`, `:off`)
52
+
53
+ ## 0.7.0
54
+
55
+ - Ruby 3 compatibility.
56
+ - Fix logging.
57
+
58
+ ## 0.6.0
59
+
60
+ - Synchronize around `evaluate_script` and `execute_script`.
61
+ - Improve logging.
62
+
63
+ ## 0.5.0
64
+
65
+ - Allow developer to signal custom async work.
66
+ - Option to wait additional tasks, to handle legacy promise implementations.
67
+ - Debugging log can be enabled during a running test.
68
+ - Also wait for images and iframes.
69
+
70
+ ## 0.4.0
71
+
72
+ - Don't fail the test when synchronization times out.
73
+ - Capybara::Lockstep.debug = true will now also enable client-side logging on the browser's JavaScript console.
74
+ - Always wait at least for `Capybara.default_max_wait_time`.
75
+
76
+ ## 0.3.2
77
+
78
+ - Delay synchronization when an alert is open (instead of failing)
79
+
80
+
81
+ ## 0.3.1
82
+
83
+ - Fix typo in log message
84
+
85
+ ## 0.3.0
86
+
87
+ - Rework entire waiting logic to be lazy.
88
+ - There is now a single method `Capybara::Lockstep.synchronize` (no distinction between awaiting "initialization" and "idle").
89
+
90
+ ## 0.2.3
91
+
92
+ - When we cannot wait for browser idle due to an open alert, wait before the next Capybara synchronize
93
+
94
+ ## 0.2.2
95
+
96
+ - Fix incorrect data in gemspec.
97
+
98
+
99
+ ## 0.2.1
100
+
101
+ - Internal changes.
102
+
103
+
104
+ ## 0.2.0
105
+
106
+ - 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.2.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  ruby2_keywords
@@ -10,32 +10,32 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activesupport (6.1.3.1)
13
+ activesupport (7.0.1)
14
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
15
15
  i18n (>= 1.6, < 2)
16
16
  minitest (>= 5.1)
17
17
  tzinfo (~> 2.0)
18
- zeitwerk (~> 2.3)
19
- addressable (2.7.0)
18
+ addressable (2.8.0)
20
19
  public_suffix (>= 2.0.2, < 5.0)
21
20
  byebug (11.1.3)
22
- capybara (3.35.3)
21
+ capybara (3.36.0)
23
22
  addressable
23
+ matrix
24
24
  mini_mime (>= 0.1.3)
25
25
  nokogiri (~> 1.8)
26
26
  rack (>= 1.6.0)
27
27
  rack-test (>= 0.6.3)
28
28
  regexp_parser (>= 1.5, < 3.0)
29
29
  xpath (~> 3.2)
30
- childprocess (3.0.0)
30
+ childprocess (4.1.0)
31
31
  chrome_remote (0.3.0)
32
32
  websocket-driver (~> 0.6)
33
- concurrent-ruby (1.1.8)
33
+ concurrent-ruby (1.1.10)
34
34
  daemons (1.3.1)
35
35
  diff-lcs (1.3)
36
36
  eventmachine (1.2.7)
37
37
  gemika (0.6.0)
38
- i18n (1.8.10)
38
+ i18n (1.10.0)
39
39
  concurrent-ruby (~> 1.0)
40
40
  jasmine (3.6.0)
41
41
  jasmine-core (~> 3.6.0)
@@ -43,18 +43,20 @@ GEM
43
43
  rack (>= 1.2.1)
44
44
  rake
45
45
  jasmine-core (3.6.0)
46
- mini_mime (1.1.0)
47
- minitest (5.14.4)
48
- nokogiri (1.11.3-x86_64-linux)
46
+ matrix (0.4.2)
47
+ mini_mime (1.1.2)
48
+ minitest (5.15.0)
49
+ nokogiri (1.13.0-x86_64-linux)
49
50
  racc (~> 1.4)
50
51
  phantomjs (2.1.1.0)
51
52
  public_suffix (4.0.6)
52
- racc (1.5.2)
53
+ racc (1.6.0)
53
54
  rack (2.2.3)
54
55
  rack-test (1.1.0)
55
56
  rack (>= 1.0, < 3)
56
57
  rake (13.0.1)
57
- regexp_parser (2.1.1)
58
+ regexp_parser (2.2.0)
59
+ rexml (3.2.5)
58
60
  rspec (3.7.0)
59
61
  rspec-core (~> 3.7.0)
60
62
  rspec-expectations (~> 3.7.0)
@@ -68,10 +70,11 @@ GEM
68
70
  diff-lcs (>= 1.2.0, < 2.0)
69
71
  rspec-support (~> 3.7.0)
70
72
  rspec-support (3.7.0)
71
- ruby2_keywords (0.0.4)
72
- rubyzip (2.3.0)
73
- selenium-webdriver (3.142.7)
74
- childprocess (>= 0.5, < 4.0)
73
+ ruby2_keywords (0.0.5)
74
+ rubyzip (2.3.2)
75
+ selenium-webdriver (4.1.0)
76
+ childprocess (>= 0.5, < 5.0)
77
+ rexml (~> 3.2, >= 3.2.5)
75
78
  rubyzip (>= 1.2.2)
76
79
  thin (1.8.0)
77
80
  daemons (~> 1.0, >= 1.0.9)
@@ -84,7 +87,6 @@ GEM
84
87
  websocket-extensions (0.1.5)
85
88
  xpath (3.2.0)
86
89
  nokogiri (~> 1.8)
87
- zeitwerk (2.4.2)
88
90
 
89
91
  PLATFORMS
90
92
  ruby
@@ -100,4 +102,4 @@ DEPENDENCIES
100
102
  thin
101
103
 
102
104
  BUNDLED WITH
103
- 2.2.15
105
+ 2.2.32
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,56 @@ 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).
60
+
61
+ capybara-lockstep synchronizes before:
62
+
63
+ - Capybara simulates a user interaction (clicking, typing, etc.)
64
+ - Capybara visits a new URL
65
+ - Capybara executes JavaScript
40
66
 
41
- Whenever Capybara visits a new URL or simulates a user interaction (clicking, typing, etc.):
67
+ When capybara-lockstep synchronizes it will:
42
68
 
43
- - capybara-lockstep waits for all document resources to load.
44
- - capybara-lockstep waits for client-side JavaScript to render or hydrate DOM elements.
45
- - capybara-lockstep waits for any AJAX requests.
69
+ - wait for all document resources to load (images, CSS, fonts, frames).
70
+ - wait for client-side JavaScript to render or hydrate DOM elements.
71
+ - wait for any pending AJAX requests to finish and their callbacks to be called.
46
72
  - 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.
73
+ - capybara-lockstep waits for dynamically inserted `<img>` or `<iframe>` elements to load.
74
+
75
+ In summary Capybara can no longer observe the page while HTTP requests are in flight.
76
+ This covers most async work that causes flaky tests.
77
+
78
+ You can also configure capybara-lockstep to [wait for other async work](#signaling-asynchronous-work) that does not involve the network, like animations.
48
79
 
49
80
 
50
81
  Installation
@@ -76,127 +107,90 @@ And then execute:
76
107
  $ bundle install
77
108
  ```
78
109
 
79
- If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
110
+ 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
111
 
81
112
 
82
113
  ### Including the JavaScript snippet
83
114
 
84
115
  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
116
 
86
- If you're using Rails you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
117
+ **If you're using Rails** you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
87
118
 
88
119
  ```erb
89
- <%= capybara_lockstep if Rails.env.test? %>
120
+ <%= capybara_lockstep if defined?(Capybara::Lockstep) %>
90
121
  ```
91
122
 
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.
123
+ Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
93
124
 
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.
125
+ **If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
95
126
 
96
- If you're not using Rails you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
127
+ **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
128
 
98
129
 
99
- ### Signaling the end of page initialization
100
130
 
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.
131
+ ### Verify successful integration
102
132
 
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.
133
+ capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
104
134
 
105
- To signal that JavaScript is still initializing, your application layouts should render the `<body>` element with an `[data-initializing]` attribute:
135
+ 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
136
 
107
- ```html
108
- <body data-initializing>
137
+ ```ruby
138
+ Capybara::Lockstep.debug = true
109
139
  ```
110
140
 
111
- Your application JavaScript should remove the `[data-initializing]` attribute when it is done rendering the initial page.
141
+ You should see messages like this in your console:
112
142
 
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.
143
+ ```text
144
+ [capybara-lockstep] Synchronizing
145
+ [capybara-lockstep] Finished waiting for JavaScript
146
+ [capybara-lockstep] Synchronized successfully
147
+ ```
114
148
 
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.**
149
+ Note that you may see some failures from tests with wrong assertions, which previously passed due to lucky timing.
116
150
 
117
- #### Example: Vanilla JS
118
151
 
119
- If all your initializing JavaScript runs synchronously on `DOMContentLoaded`, you can remove `[data-initializing]` in an event handler:
120
152
 
121
- ```js
122
- document.addEventListener('DOMContentLoaded', function() {
123
- // Initialize the page here
124
- document.body.removeAttribute('data-initializing')
125
- })
126
- ```
153
+ ## Signaling asynchronous work
127
154
 
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:
155
+ By default capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
129
156
 
130
- ```js
131
- document.addEventListener('DOMContentLoaded', function() {
132
- Libary.initialize({
133
- onFinished: function() {
134
- document.body.removeAttribute('data-initializing')
135
- }
136
- })
137
- })
138
- ```
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]`:
157
+ You can configure capybara-lockstep to wait for other async work that does not involve the network. Let's say we have an animation that fades in a new element over 2 seconds. The following will prevent Capybara from observing the page while the animation is running:
141
158
 
142
159
  ```js
143
- document.addEventListener('DOMContentLoaded', function() {
144
- Libary.initialize()
145
- setTimeout(function() { document.body.removeAttribute('data-initializing') })
146
- })
160
+ async function fadeIn(element) {
161
+ CapybaraLockstep.startWork('Animation')
162
+ startAnimation(element, 'fade-in')
163
+ await waitForAnimationEnd(element)
164
+ CapybaraLockstep.stopWork('Animation')
165
+ }
147
166
  ```
148
167
 
149
- If your initialization code lazy-loads another script, you should only remove `[data-initializing]` once that is done:
168
+ 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
169
 
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
- })
170
+ ```text
171
+ [capybara-lockstep] Started work: Animation [1 jobs]
172
+ [capybara-lockstep] Finished work: Animation [0 jobs]
158
173
  ```
159
174
 
175
+ You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
160
176
 
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
177
 
165
- ```js
166
- up.compiler('body', function(body) {
167
- body.removeAttribute('data-initializing')
168
- })
169
- ```
170
-
171
- #### Example: AngularJS 1
178
+ ## Note on interacting with the JavaScript API
172
179
 
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]`:
180
+ 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
181
 
175
182
  ```js
176
- app.directive('body', function() {
177
- return {
178
- restrict: 'E',
179
- link: function() {
180
- document.body.removeAttribute('data-initializing')
181
- }
182
- }
183
- })
183
+ if (window.CapybaraLockstep) {
184
+ // interact with CapybaraLockstep
185
+ }
184
186
  ```
185
187
 
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:
188
+ If you can use ES6 you may also use [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) to only call a function if `window.CapybaraLockstep` is defined:
191
189
 
192
- ```text
193
- [capybara-lockstep] Synchronizing
194
- [capybara-lockstep] Finished waiting for JavaScript
195
- [capybara-lockstep] Synchronized successfully
190
+ ```js
191
+ window.CapybaraLockstep?.startWork('Work')
196
192
  ```
197
193
 
198
- Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
199
-
200
194
 
201
195
  ## Performance impact
202
196
 
@@ -204,7 +198,7 @@ capybara-lockstep may or may not impact the runtime of your test suite. It depen
204
198
 
205
199
  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
200
 
207
- In casual testing I experienced a performance impact between +/- 10%.
201
+ In casual testing with large test suites I experienced a performance impact between +/- 10%.
208
202
 
209
203
 
210
204
  ## Debugging log
@@ -243,9 +237,9 @@ Capybara::Lockstep.debug = Rails.logger
243
237
 
244
238
  ### Logging in the browser only
245
239
 
246
- To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
240
+ To enable logging in the browser console (but not STDOUT), include the [JavaScript snippet](#including-the-javascript-snippet) with `{ debug: true }`:
247
241
 
248
- ```
242
+ ```ruby
249
243
  capybara_lockstep(debug: true)
250
244
  ```
251
245
 
@@ -253,7 +247,11 @@ capybara_lockstep(debug: true)
253
247
 
254
248
  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
249
 
256
- When synchronization times out, capybara-lockstep will log but not raise an error.
250
+ When synchronization times out, capybara-lockstep will [log](#debugging-log):
251
+
252
+ ```text
253
+ [capybara-lockstep] Could not synchronize within 3 seconds
254
+ ```
257
255
 
258
256
  You can configure a different timeout:
259
257
 
@@ -261,10 +259,19 @@ You can configure a different timeout:
261
259
  Capybara::Lockstep.timeout = 5 # seconds
262
260
  ```
263
261
 
264
- To revert to defaulting to `Capybara.default_max_wait_time`, set the timeout to `nil`:
262
+ 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.
263
+
264
+ If you want to be strict you may configure that an `Capybara::Lockstep::Timeout` error is raised after a timeout:
265
+
266
+ ```ruby
267
+ Capybara::Lockstep.timeout_with = :error
268
+ ```
269
+
270
+ To revert to defaults:
265
271
 
266
272
  ```ruby
267
273
  Capybara::Lockstep.timeout = nil
274
+ Capybara::Lockstep.timeout_with = nil
268
275
  ```
269
276
 
270
277
 
@@ -300,9 +307,9 @@ ensure
300
307
  end
301
308
  ```
302
309
 
303
- Note that you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
310
+ In the `:manual` mode you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
304
311
 
305
- To completely disable synchronization:
312
+ To completely disable synchronization, even when `Capybara::Lockstep.synchronize` is called:
306
313
 
307
314
  ```ruby
308
315
  Capybara::Lockstep.mode = :off
@@ -310,70 +317,59 @@ Capybara::Lockstep.synchronize # will not synchronize
310
317
  ```
311
318
 
312
319
 
313
- ## Signaling asynchronous work
314
320
 
315
- If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
321
+ ## Handling legacy promises
316
322
 
317
- ```js
318
- CapybaraLockstep.startWork('Eject warp core')
319
- doAsynchronousWork().then(function() {
320
- CapybaraLockstep.stopWork('Eject warp core')
321
- })
322
- ```
323
+ Legacy promise implementations (like jQuery's `$.Deferred` and AngularJS' `$q`) work using [tasks instead of microtasks](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). Their AJAX implementations (like `$.ajax()` and `$http`) use task-based promises to signal that a request is done.
323
324
 
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
+ This means there is a time window in which all AJAX requests have finished, but their callbacks have not yet run:
325
326
 
326
- ```text
327
- [capybara-lockstep] Started work: Eject warp core [1 jobs]
328
- [capybara-lockstep] Finished work: Eject warp core [0 jobs]
327
+ ```js
328
+ $http.get('/foo').then(function() {
329
+ // This callback runs one task after the response was received
330
+ })
329
331
  ```
330
332
 
331
- You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
332
-
333
+ It is theoretically possible that your test will observe the browser in that window, and expect content that has not been rendered yet. Affected code must call `then()` on a task-based promise **or** use `setTimeout()` to push work into the next task.
333
334
 
334
- ## Note on interacting with the JavaScript API
335
+ Any issues caused by 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:
335
336
 
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
- }
337
+ ```ruby
338
+ Capybara::Lockstep.wait_tasks = 1
342
339
  ```
343
340
 
344
- ## Handling legacy promises
341
+ If you see longer chains of `then()` or nested `setTimeout()` calls in your code, you may need to configure a higher number of tasks to wait.
345
342
 
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.
343
+ Waiting additional tasks will have a negative performance impact on your test suite.
347
344
 
348
- This means there is a time window in which all AJAX requests have finished, but their callbacks have not yet run:
345
+ > **Note:** When capybara-lockstep detects jQuery on the page, it will automatically patch [`$.ajax()`](https://api.jquery.com/jQuery.ajax/) to wait an additional task after the response was received. If your only concern is callbacks to `$.ajax()` you do not need so set `Capybara::Lockstep.wait_tasks`.
349
346
 
350
- ```js
351
- $.ajax('/foo').then(function() {
352
- // This callback runs one task after the response was received
353
- })
354
- ```
355
347
 
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:
348
+ ## Contributing
357
349
 
358
- ```js
359
- Capybara:Lockstep.wait_tasks = 1
360
- ```
350
+ Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
361
351
 
362
- If you see longer `then()` chains in your code, you may need to configure a higher number of tasks to wait.
352
+ After checking out the repo, run `bin/setup` to install dependencies.
363
353
 
364
- This will have a negative performance impact on your test suite.
354
+ Then, run `rake spec` to run the tests.
365
355
 
356
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
366
357
 
367
- ## Development
358
+ ### Manually testing a change
368
359
 
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.
360
+ To test an unrelased change with a test suite, we recommend to temporarily link the local repository from your test suites's `Gemfile`:
370
361
 
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).
362
+ ```ruby
363
+ gem 'capybara-lockstep', path: '../capybara-lockstep'
364
+ ```
372
365
 
366
+ As an alternative you may also install this gem onto your local machine by running `bundle exec rake install`.
373
367
 
374
- ## Contributing
368
+ ### Releasing a new version
375
369
 
376
- Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
370
+ - Update the version number in `version.rb`
371
+ - 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).
372
+ - If RubyGems publishing seems to freeze, try entering your OTP code.
377
373
 
378
374
 
379
375
  ## License
@@ -14,6 +14,10 @@ Gem::Specification.new do |spec|
14
14
  spec.metadata["homepage_uri"] = spec.homepage
15
15
  spec.metadata["source_code_uri"] = spec.homepage
16
16
 
17
+ spec.metadata["bug_tracker_uri"] = "https://github.com/makandra/capybara-lockstep/issues"
18
+ spec.metadata["changelog_uri"] = "https://github.com/makandra/capybara-lockstep/blob/master/CHANGELOG.md"
19
+ spec.metadata["rubygems_mfa_required"] = 'true'
20
+
17
21
  # Specify which files should be added to the gem when it is released.
18
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
23
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -1,34 +1,99 @@
1
1
  require 'ruby2_keywords'
2
2
 
3
+ module Capybara
4
+ module Lockstep
5
+ module UnsychronizeAfter
6
+ def unsychronize_after(meth)
7
+ mod = Module.new do
8
+ define_method meth do |*args, &block|
9
+ super(*args, &block)
10
+ ensure
11
+ Lockstep.synchronized = false
12
+ end
13
+
14
+ ruby2_keywords meth
15
+ end
16
+
17
+ prepend(mod)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module Capybara
24
+ module Lockstep
25
+ module SynchronizeBefore
26
+ def synchronize_before(meth, lazy:)
27
+ mod = Module.new do
28
+ define_method meth do |*args, &block|
29
+ Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
30
+ super(*args, &block)
31
+ end
32
+
33
+ ruby2_keywords meth
34
+ end
35
+
36
+ prepend(mod)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ Capybara::Session.class_eval do
43
+ extend Capybara::Lockstep::SynchronizeBefore
44
+ extend Capybara::Lockstep::UnsychronizeAfter
45
+
46
+ synchronize_before :html, lazy: true # wait until running JavaScript has updated the DOM
47
+
48
+ synchronize_before :current_url, lazy: true # wait until running JavaScript has updated the URL
49
+
50
+ synchronize_before :refresh, lazy: false # wait until running JavaScript has updated the URL
51
+ unsychronize_after :refresh # new document is no longer synchronized
52
+
53
+ synchronize_before :go_back, lazy: false # wait until running JavaScript has updated the URL
54
+ unsychronize_after :go_back # new document is no longer synchronized
55
+
56
+ synchronize_before :go_forward, lazy: false # wait until running JavaScript has updated the URL
57
+ unsychronize_after :go_forward # new document is no longer synchronized
58
+
59
+ synchronize_before :switch_to_frame, lazy: true # wait until the current frame is done processing
60
+ unsychronize_after :switch_to_frame # now that we've switched into the new frame, we don't know the document's synchronization state.
61
+
62
+ synchronize_before :switch_to_window, lazy: true # wait until the current frame is done processing
63
+ unsychronize_after :switch_to_window # now that we've switched to the new window, we don't know the document's synchronization state.
64
+ end
65
+
3
66
  module Capybara
4
67
  module Lockstep
5
68
  module VisitWithWaiting
6
- ruby2_keywords def visit(*args, &block)
69
+ def visit(*args, &block)
7
70
  url = args[0]
8
71
  # Some of our apps have a Cucumber step that changes drivers mid-scenario.
9
72
  # It works by creating a new Capybara session and re-visits the URL from the
10
73
  # previous session. If this happens before a URL is ever loaded,
11
74
  # it re-visits the URL "data:", which will never "finish" initializing.
12
75
  # Also when opening a new tab via Capybara, the initial URL is about:blank.
13
- visiting_remote_url = !(url.start_with?('data:') || url.start_with?('about:'))
76
+ visiting_real_url = !(url.start_with?('data:') || url.start_with?('about:'))
14
77
 
15
- if visiting_remote_url
78
+ if visiting_real_url
16
79
  # We're about to leave this screen, killing all in-flight requests.
17
80
  # Give pending form submissions etc. a chance to finish before we tear down
18
81
  # the browser environment.
19
82
  #
20
83
  # We force a non-lazy synchronization so we pick up all client-side changes
21
84
  # that have not been caused by Capybara commands.
22
- Lockstep.synchronize(lazy: false)
85
+ Lockstep.auto_synchronize(lazy: false, log: "Synchronizing before visiting #{url}")
23
86
  end
24
87
 
25
88
  super(*args, &block).tap do
26
- if visiting_remote_url
89
+ if visiting_real_url
27
90
  # We haven't yet synchronized the new screen.
28
91
  Lockstep.synchronized = false
29
92
  end
30
93
  end
31
94
  end
95
+
96
+ ruby2_keywords :visit
32
97
  end
33
98
  end
34
99
  end
@@ -58,14 +123,15 @@ module Capybara
58
123
  Lockstep.auto_synchronize(lazy: !script_may_navigate_away, log: "Synchronizing before script: #{script}")
59
124
  end
60
125
 
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
126
+ super(script, *args, &block)
127
+ ensure
128
+ if !Lockstep.synchronizing?
129
+ # We haven't yet synchronized with whatever changes the JavaScript
130
+ # did on the frontend.
131
+ Lockstep.synchronized = false
67
132
  end
68
133
  end
134
+
69
135
  ruby2_keywords meth
70
136
  end
71
137
  prepend(mod)
@@ -84,24 +150,6 @@ Capybara::Session.class_eval do
84
150
  # internally and we don't want to synchronize multiple times.
85
151
  end
86
152
 
87
- module Capybara
88
- module Lockstep
89
- module UnsychronizeAfter
90
- def unsychronize_after(meth)
91
- mod = Module.new do
92
- define_method meth do |*args, &block|
93
- super(*args, &block).tap do
94
- Lockstep.synchronized = false
95
- end
96
- end
97
- ruby2_keywords meth
98
- end
99
- prepend(mod)
100
- end
101
- end
102
- end
103
- end
104
-
105
153
  # Capybara 3 has driver-specific Node classes which sometimes
106
154
  # super to Capybara::Selenium::Node, but not always.
107
155
  node_classes = [
@@ -141,14 +189,16 @@ end
141
189
  module Capybara
142
190
  module Lockstep
143
191
  module SynchronizeWithCatchUp
144
- ruby2_keywords def synchronize(*args, &block)
192
+ def synchronize(*args, &block)
145
193
  # This method is called by Capybara before most interactions with
146
194
  # the browser. It is a different method than Capybara::Lockstep.synchronize!
147
195
  # We use the { lazy } option to only synchronize when we're out of sync.
148
- Capybara::Lockstep.auto_synchronize(lazy: true)
196
+ Lockstep.auto_synchronize(lazy: true, log: 'Synchronizing before node access')
149
197
 
150
198
  super(*args, &block)
151
199
  end
200
+
201
+ ruby2_keywords :synchronize
152
202
  end
153
203
  end
154
204
  end
@@ -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
@@ -3,12 +3,14 @@ window.CapybaraLockstep = (function() {
3
3
  let debug
4
4
  let jobCount
5
5
  let idleCallbacks
6
+ let finishedWorkTags
6
7
  let waitTasks
7
8
  reset()
8
9
 
9
10
  function reset() {
10
11
  jobCount = 0
11
12
  idleCallbacks = []
13
+ finishedWorkTags = []
12
14
  waitTasks = 0
13
15
  debug = false
14
16
  }
@@ -74,12 +76,17 @@ window.CapybaraLockstep = (function() {
74
76
  jobCount--
75
77
 
76
78
  if (tag) {
79
+ finishedWorkTags.push(tag)
77
80
  logPositive('Finished work: %s [%d jobs]', tag, jobCount)
78
81
  }
79
82
 
80
- let idleCallback
81
- while (isIdle() && (idleCallback = idleCallbacks.shift())) {
82
- idleCallback('Finished waiting for browser')
83
+ if (isIdle()) {
84
+ let idleCallback
85
+ while ((idleCallback = idleCallbacks.shift())) {
86
+ idleCallback("Finished waiting for " + finishedWorkTags.join(', '))
87
+ }
88
+
89
+ finishedWorkTags = []
83
90
  }
84
91
  }
85
92
 
@@ -123,43 +130,21 @@ window.CapybaraLockstep = (function() {
123
130
  }
124
131
  }
125
132
 
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
133
  function trackRemoteElements() {
150
134
  if (!window.MutationObserver) {
151
135
  return
152
136
  }
153
137
 
154
- // Dynamic imports or analytics snippets may insert a <script src>
155
- // tag that loads and executes additional JavaScript. We want to be isBusy()
138
+ // Dynamic imports or analytics snippets may insert a script element
139
+ // that loads and executes additional JavaScript. We want to be isBusy()
156
140
  // until such scripts have loaded or errored.
157
141
  let observer = new MutationObserver(onAnyElementChanged)
158
142
  observer.observe(document, { subtree: true, childList: true })
159
143
  }
160
144
 
161
145
  function trackJQuery() {
162
- // jQuery may be loaded after us, so we wait until DOMContentReady.
146
+ // CapybaraLockstep.track() is called as the first script in the head.
147
+ // jQuery will be loaded after us, so we wait until DOMContentReady.
163
148
  whenReady(function() {
164
149
  if (!window.jQuery || waitTasks > 0) {
165
150
  return
@@ -180,28 +165,6 @@ window.CapybaraLockstep = (function() {
180
165
  })
181
166
  }
182
167
 
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
168
  function isRemoteScript(element) {
206
169
  if (element.tagName === 'SCRIPT') {
207
170
  let src = element.getAttribute('src')
@@ -303,13 +266,28 @@ window.CapybaraLockstep = (function() {
303
266
  }
304
267
  }
305
268
 
269
+ function trackOldUnpoly() {
270
+ // CapybaraLockstep.track() is called as the first script in the head.
271
+ // Unpoly will be loaded after us, so we wait until DOMContentReady.
272
+ whenReady(function() {
273
+ // Unpoly 0.x would wait one task after DOMContentLoaded before booting.
274
+ // There's a slim chance that Capybara can observe the page before compilers have run.
275
+ // Unpoly 1.0+ runs compilers on DOMContentLoaded, so there's no issue.
276
+ if (window.up?.version?.startsWith('0.')) {
277
+ startWork('Old Unpoly')
278
+ setTimeout(function () {
279
+ stopWork('Old Unpoly')
280
+ })
281
+ }
282
+ })
283
+ }
284
+
306
285
  function track() {
286
+ trackOldUnpoly()
307
287
  trackFetch()
308
288
  trackXHR()
309
- trackInteraction()
310
289
  trackRemoteElements()
311
290
  trackJQuery()
312
- trackHydration()
313
291
  }
314
292
 
315
293
  function synchronize(callback) {
@@ -13,7 +13,17 @@ module Capybara
13
13
  alias synchronizing? synchronizing
14
14
 
15
15
  def synchronized?
16
+ # The synchronized flag is per-session (page == Capybara.current_session).
17
+ # This enables tests that use more than one browser, e.g. to test multi-user interaction:
18
+ # https://makandracards.com/makandra/474480-how-to-make-a-cucumber-test-work-with-multiple-browser-sessions
19
+ #
20
+ # Ideally the synchronized flag would also be per-tab, per-frame and per-document.
21
+ # We haven't found a way to patch this into Capybara, as there does not seem to be
22
+ # a persistent object representing a document. Capybara::Node::Document just seems to
23
+ # be a proxy accessing whatever is the current document. The way we work around this
24
+ # is that we synchronize before switching tabs or frames.
16
25
  value = page.instance_variable_get(:@lockstep_synchronized)
26
+
17
27
  # We consider a new Capybara session to be synchronized.
18
28
  # This will be set to false after our first visit().
19
29
  value.nil? ? true : value
@@ -24,19 +34,30 @@ module Capybara
24
34
  end
25
35
 
26
36
  def synchronize(lazy: false, log: nil)
37
+ # The { lazy } option is a performance optimization that will prevent capybara-lockstep
38
+ # from synchronizing multiple times in expressions like `page.find('.foo').find('.bar')`.
39
+ # The { lazy } option has nothing todo with :auto mode.
40
+ #
41
+ # With { lazy: true } we only synchronize when the Ruby-side thinks we're out of sync.
42
+ # This saves us an expensive execute_script() roundtrip that goes to the browser and back.
43
+ # However the knowledge of the Ruby-side is limited: We only assume that we're out of sync
44
+ # after a page load or after a Capybara command. There may be additional client-side work
45
+ # that the Ruby-side is not aware of, e.g. an AJAX call scheduled by a timeout.
46
+ #
47
+ # With { lazy: false } we force synchronization with the browser, whether the Ruby-side
48
+ # thinks we're in sync or not. This always makes an execute_script() rountrip, but picks up
49
+ # non-lazy synchronization so we pick up client-side work that have not been caused
50
+ # by Capybara commands.
27
51
  if (lazy && synchronized?) || synchronizing? || mode == :off
28
52
  return
29
53
  end
30
54
 
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
-
37
- synchronize_now
55
+ synchronize_now(log: log)
38
56
  end
39
57
 
58
+ # Automatic synchronization from within the capybara-lockstep should always call #auto_synchronize.
59
+ # This only synchronizes IFF in :auto mode, i.e. the user has not explicitly disabled automatic syncing.
60
+ # The :auto mode has nothing to do with the { lazy } option.
40
61
  def auto_synchronize(**options)
41
62
  if mode == :auto
42
63
  synchronize(**options)
@@ -45,11 +66,11 @@ module Capybara
45
66
 
46
67
  private
47
68
 
48
- def synchronize_now
69
+ def synchronize_now(log: 'Synchronizing')
49
70
  self.synchronizing = true
50
71
  self.synchronized = false
51
72
 
52
- log 'Synchronizing'
73
+ self.log(log)
53
74
 
54
75
  start_time = current_seconds
55
76
 
@@ -68,6 +89,8 @@ module Capybara
68
89
  if (protocol === 'data:' || protocol == 'about:') {
69
90
  done(#{ERROR_PAGE_MISSING.to_json})
70
91
  } else if (document.readyState === 'complete') {
92
+ // WebDriver always waits for the `load` event after a visit(),
93
+ // unless a different page load strategy was configured.
71
94
  synchronize()
72
95
  } else {
73
96
  window.addEventListener('load', synchronize)
@@ -88,9 +111,14 @@ module Capybara
88
111
  end
89
112
  end
90
113
  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.
114
+ timeout_message = "Could not synchronize within #{timeout} seconds"
115
+ log timeout_message
116
+ if timeout_with == :error
117
+ raise Timeout, timeout_message
118
+ else
119
+ # Don't raise an error, this may happen if the server is slow to respond.
120
+ # We will retry on the next Capybara synchronize call.
121
+ end
94
122
  rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
95
123
  log ERROR_ALERT_OPEN
96
124
  # 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.2.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.2.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: 2022-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -101,6 +101,9 @@ licenses:
101
101
  metadata:
102
102
  homepage_uri: https://github.com/makandra/capybara-lockstep
103
103
  source_code_uri: https://github.com/makandra/capybara-lockstep
104
+ bug_tracker_uri: https://github.com/makandra/capybara-lockstep/issues
105
+ changelog_uri: https://github.com/makandra/capybara-lockstep/blob/master/CHANGELOG.md
106
+ rubygems_mfa_required: 'true'
104
107
  post_install_message:
105
108
  rdoc_options: []
106
109
  require_paths: