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 +4 -4
- data/.github/workflows/test.yml +6 -0
- data/CHANGELOG.md +93 -20
- data/Gemfile.lock +21 -19
- data/README.md +131 -135
- data/capybara-lockstep.gemspec +4 -0
- data/lib/capybara-lockstep/capybara_ext.rb +81 -31
- data/lib/capybara-lockstep/configuration.rb +9 -1
- data/lib/capybara-lockstep/errors.rb +1 -1
- data/lib/capybara-lockstep/helper.js +31 -53
- data/lib/capybara-lockstep/lockstep.rb +40 -12
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec0d3874212ec29a338fa16ef831e76c0b8dc65b4a200f3a305b7ce41bd76957
|
4
|
+
data.tar.gz: b5d48346fb7224b2154512534757ef96638f1df9cc62f9d0f911aac8d6a5f846
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d44c17fa50ca0fd386b34ecea302148989f4d48d63c48820bcfff75bcd51c63cfbacea0053e63fc1c27f6347fbe252f14be83372aed123a6a1c2574328fe42e
|
7
|
+
data.tar.gz: 9e7f5cfcbb6b3750277bc55c37484e17859aa4d96f1b8f7b6c1865f4467ae00fad5311e11ea0bfc73d5b8ec5c925df5c8ec6bb2473da6b431296736c89972794
|
data/.github/workflows/test.yml
CHANGED
@@ -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
|
-
|
7
|
+
### Synchronization around history navigation
|
7
8
|
|
8
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
+
- `switch_to_frame`
|
23
|
+
- `within_frame`
|
24
|
+
- `switch_to_window`
|
25
|
+
- `within_window`
|
19
26
|
|
20
|
-
|
21
|
-
- add gemika for tests with github actions
|
22
|
-
- add Ruby 3 support
|
27
|
+
### Improved logging
|
23
28
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
##
|
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.
|
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 (
|
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
|
-
|
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.
|
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 (
|
30
|
+
childprocess (4.1.0)
|
31
31
|
chrome_remote (0.3.0)
|
32
32
|
websocket-driver (~> 0.6)
|
33
|
-
concurrent-ruby (1.1.
|
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.
|
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
|
-
|
47
|
-
|
48
|
-
|
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.
|
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.
|
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.
|
72
|
-
rubyzip (2.3.
|
73
|
-
selenium-webdriver (
|
74
|
-
childprocess (>= 0.5, <
|
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.
|
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
|
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
|
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
|
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
|
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
|
-
|
67
|
+
When capybara-lockstep synchronizes it will:
|
42
68
|
|
43
|
-
-
|
44
|
-
-
|
45
|
-
-
|
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
|
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.
|
123
|
+
Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
|
93
124
|
|
94
|
-
If you
|
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
|
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
|
-
|
131
|
+
### Verify successful integration
|
102
132
|
|
103
|
-
capybara-lockstep will
|
133
|
+
capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
|
104
134
|
|
105
|
-
|
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
|
-
```
|
108
|
-
|
137
|
+
```ruby
|
138
|
+
Capybara::Lockstep.debug = true
|
109
139
|
```
|
110
140
|
|
111
|
-
|
141
|
+
You should see messages like this in your console:
|
112
142
|
|
113
|
-
|
143
|
+
```text
|
144
|
+
[capybara-lockstep] Synchronizing
|
145
|
+
[capybara-lockstep] Finished waiting for JavaScript
|
146
|
+
[capybara-lockstep] Synchronized successfully
|
147
|
+
```
|
114
148
|
|
115
|
-
|
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
|
-
|
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
|
-
|
155
|
+
By default capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
|
129
156
|
|
130
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
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
|
-
```
|
152
|
-
|
153
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
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
|
-
```
|
193
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
321
|
+
## Handling legacy promises
|
316
322
|
|
317
|
-
|
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
|
-
|
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
|
-
```
|
327
|
-
|
328
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
343
|
+
Waiting additional tasks will have a negative performance impact on your test suite.
|
347
344
|
|
348
|
-
|
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
|
-
|
348
|
+
## Contributing
|
357
349
|
|
358
|
-
|
359
|
-
Capybara:Lockstep.wait_tasks = 1
|
360
|
-
```
|
350
|
+
Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
|
361
351
|
|
362
|
-
|
352
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
363
353
|
|
364
|
-
|
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
|
-
|
358
|
+
### Manually testing a change
|
368
359
|
|
369
|
-
|
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
|
-
|
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
|
-
|
368
|
+
### Releasing a new version
|
375
369
|
|
376
|
-
|
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
|
data/capybara-lockstep.gemspec
CHANGED
@@ -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
|
-
|
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
|
-
|
76
|
+
visiting_real_url = !(url.start_with?('data:') || url.start_with?('about:'))
|
14
77
|
|
15
|
-
if
|
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.
|
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
|
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)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|
-
|
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)
|
@@ -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
|
-
|
81
|
-
|
82
|
-
idleCallback
|
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
|
155
|
-
//
|
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
|
-
//
|
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
|
-
|
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
|
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
|
-
|
92
|
-
|
93
|
-
|
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.
|
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.
|
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:
|
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:
|