capybara-lockstep 0.2.2 → 0.3.3

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: 58ff0d51fafccaca566a89722c750b18db169331d57070c5db7d5de32620d648
4
- data.tar.gz: f6f859ded650d9b2dba0c43bbb7b19333e505ed57aa0d883b0a2981e5928f1a8
3
+ metadata.gz: 86fa6ca6223224f8ac4b53c973cf961287964d11b129d8aa89b7b4dc81d4bba4
4
+ data.tar.gz: f250e201b8ef467fd46d1e30654d8e2c88366c23ad8cb314e1031cb44268dcd7
5
5
  SHA512:
6
- metadata.gz: 4c1ba4546483509650a7423d04e0acffaf286db556ae431152076478e5d2ca7611a915e9f666c077d9c9c205cb4703d2cf29341e52a0b39493c4b11b78e6a883
7
- data.tar.gz: 1e890d3d985af8ce4d623778781cfc06a1242d83d1c93b98bc550997e3044066544dc15100ccdb28e89090e1b865d7720200a84f8461984655b0f87fcf795281
6
+ metadata.gz: 846816f82797d02dc81869e449afaf99c3fdfe89682d6584a56b3416cbb5942138a38ae457dfebcbb4fc7f20ed2673766a02287911b258445c5a9b800d613f9c
7
+ data.tar.gz: 30064e290e2caed298e5868b0f1cb9c1f5aa34444895fa8cefd4d283b0433528dd1ee7793526f0652fbc11511ed3e50e9a6eb999b00f6a898411480616422199
data/Gemfile CHANGED
@@ -8,3 +8,5 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+
12
+ gem 'byebug'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (0.2.2)
4
+ capybara-lockstep (0.3.3)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  selenium-webdriver (>= 3)
@@ -9,13 +9,15 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (5.2.4.3)
12
+ activesupport (6.1.3)
13
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
- i18n (>= 0.7, < 2)
15
- minitest (~> 5.1)
16
- tzinfo (~> 1.1)
14
+ i18n (>= 1.6, < 2)
15
+ minitest (>= 5.1)
16
+ tzinfo (~> 2.0)
17
+ zeitwerk (~> 2.3)
17
18
  addressable (2.7.0)
18
19
  public_suffix (>= 2.0.2, < 5.0)
20
+ byebug (11.1.3)
19
21
  capybara (3.35.3)
20
22
  addressable
21
23
  mini_mime (>= 0.1.3)
@@ -25,13 +27,15 @@ GEM
25
27
  regexp_parser (>= 1.5, < 3.0)
26
28
  xpath (~> 3.2)
27
29
  childprocess (3.0.0)
28
- concurrent-ruby (1.1.7)
30
+ concurrent-ruby (1.1.8)
29
31
  diff-lcs (1.3)
30
- i18n (1.8.2)
32
+ i18n (1.8.9)
31
33
  concurrent-ruby (~> 1.0)
32
34
  mini_mime (1.0.2)
33
- minitest (5.14.1)
34
- nokogiri (1.11.1-x86_64-linux)
35
+ mini_portile2 (2.5.0)
36
+ minitest (5.14.4)
37
+ nokogiri (1.11.1)
38
+ mini_portile2 (~> 2.5.0)
35
39
  racc (~> 1.4)
36
40
  public_suffix (4.0.6)
37
41
  racc (1.5.2)
@@ -53,20 +57,21 @@ GEM
53
57
  diff-lcs (>= 1.2.0, < 2.0)
54
58
  rspec-support (~> 3.7.0)
55
59
  rspec-support (3.7.0)
56
- rubyzip (1.3.0)
60
+ rubyzip (2.3.0)
57
61
  selenium-webdriver (3.142.7)
58
62
  childprocess (>= 0.5, < 4.0)
59
63
  rubyzip (>= 1.2.2)
60
- thread_safe (0.3.6)
61
- tzinfo (1.2.7)
62
- thread_safe (~> 0.1)
64
+ tzinfo (2.0.4)
65
+ concurrent-ruby (~> 1.0)
63
66
  xpath (3.2.0)
64
67
  nokogiri (~> 1.8)
68
+ zeitwerk (2.4.2)
65
69
 
66
70
  PLATFORMS
67
71
  ruby
68
72
 
69
73
  DEPENDENCIES
74
+ byebug
70
75
  capybara-lockstep!
71
76
  rake (~> 13.0)
72
77
  rspec (~> 3.0)
data/README.md CHANGED
@@ -59,7 +59,7 @@ Installation
59
59
  Check if your application satisfies all requirements for capybara-lockstep:
60
60
 
61
61
  - Capybara 2 or higher.
62
- - Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-headless deactivates itself for any other driver.
62
+ - Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-lockstep deactivates itself for any other driver.
63
63
  - This gem was only tested with a Selenium-controlled Chrome browser. [Chrome in headless mode](https://makandracards.com/makandra/492109-running-capybara-tests-in-headless-chrome) is recommended, but not required.
64
64
  - This gem was only tested with Rails, but there's no Rails dependency.
65
65
 
@@ -170,9 +170,13 @@ app.directive('body', function() {
170
170
 
171
171
  capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
172
172
 
173
- Run your test suite to see if integration was successful and whether stability improves.
173
+ Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber). You should see messages like this in your console:
174
174
 
175
- When you run into issues or don't see an effect, try activating `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
175
+ ```text
176
+ [capybara-lockstep] Synchronizing
177
+ [capybara-lockstep] Finished waiting for JavaScript
178
+ [capybara-lockstep] Synchronized successfully
179
+ ```
176
180
 
177
181
  Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
178
182
 
@@ -183,7 +187,7 @@ capybara-lockstep may or may not impact the runtime of your test suite. It depen
183
187
 
184
188
  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.
185
189
 
186
- In casual testing I experienced a negative performance impact between 0% and 10%.
190
+ In casual testing I experienced a performance impact between +/- 10%.
187
191
 
188
192
 
189
193
  ## Debugging log
@@ -197,7 +201,9 @@ Capybara::Lockstep.debug = true
197
201
  You should now see messages like this during your test runs:
198
202
 
199
203
  ```
200
- [Capybara::Lockstep] JavaScript or AJAX requests are running
204
+ [capybara-lockstep] Synchronizing
205
+ [capybara-lockstep] Finished waiting for JavaScript
206
+ [capybara-lockstep] Synchronized successfully
201
207
  ```
202
208
 
203
209
  You may also configure logging to an existing logger object:
@@ -220,7 +226,6 @@ ensure
220
226
  end
221
227
  ```
222
228
 
223
-
224
229
  ## Timeout
225
230
 
226
231
  By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
@@ -231,8 +236,28 @@ You can configure a different timeout:
231
236
  Capybara::Lockstep.timeout = 5 # seconds
232
237
  ```
233
238
 
239
+ ## Ruby API
240
+
241
+ capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
242
+
243
+ For additional edge cases you may interact with capybara-lockstep from your Ruby code.
244
+
245
+
246
+ ### Waiting until the browser is idle
234
247
 
248
+ This will block until the document was loaded, the DOM has been hydrated and all AJAX requests have concluded:
235
249
 
250
+ ```ruby
251
+ Capybara::Lockstep.synchronize
252
+ ```
253
+
254
+ An example use case is a Cucumber step that explicitely waits for JavaScript to finish, in the rare occasion where capybara-lockstep hasn't picked up an event or request:
255
+
256
+ ```gherkin
257
+ When 'I wait for the page to load' do
258
+ Capybara::Lockstep.synchronize
259
+ end
260
+ ```
236
261
 
237
262
  ## JavaScript API
238
263
 
@@ -270,55 +295,29 @@ CapybaraLockstep.isIdle() // => true
270
295
 
271
296
  ### Waiting until the browser is idle
272
297
 
273
- ```js
274
- CapybaraLockstep.awaitIdle(callback)
275
- ```
298
+ This will run the given callback once the browser is considered to be idle:
276
299
 
277
- ## Ruby API
278
-
279
- capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
280
-
281
- For additional edge cases you may interact with capybara-lockstep from your Ruby code.
282
-
283
-
284
- ### Waiting until the browser is idle
285
-
286
- This will block until the document was loaded and the DOM has been hydrated:
287
-
288
- ```ruby
289
- Capybara::Lockstep.await_initialized
290
- ```
291
-
292
- This will block while the browser is busy with JavaScript and AJAX requests:
293
-
294
- ```ruby
295
- Capybara::Lockstep.await_idle
296
- ```
297
-
298
- ### Checking if the browser is busy
299
-
300
- You can query capybara-lockstep whether it considers the browser to be busy or idle:
301
-
302
- ```ruby
303
- Capybara::Lockstep.idle? # => true
304
- Capybara::Lockstep.busy? # => false
300
+ ```js
301
+ CapybaraLockstep.synchronize(callback)
305
302
  ```
306
303
 
307
-
308
304
  ## Development
309
305
 
310
306
  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.
311
307
 
312
308
  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).
313
309
 
310
+
314
311
  ## Contributing
315
312
 
316
313
  Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
317
314
 
315
+
318
316
  ## License
319
317
 
320
318
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
321
319
 
320
+
322
321
  ## Credits
323
322
 
324
323
  Henning Koch ([@triskweline](https://twitter.com/triskweline)) from [makandra](https://makandra.com).
@@ -10,8 +10,8 @@ end
10
10
 
11
11
  require_relative 'capybara-lockstep/version'
12
12
  require_relative 'capybara-lockstep/errors'
13
- require_relative 'capybara-lockstep/patiently'
14
13
  require_relative 'capybara-lockstep/configuration'
14
+ require_relative 'capybara-lockstep/logging'
15
15
  require_relative 'capybara-lockstep/lockstep'
16
16
  require_relative 'capybara-lockstep/capybara_ext'
17
17
  require_relative 'capybara-lockstep/helper'
@@ -1,26 +1,44 @@
1
1
  module Capybara
2
2
  module Lockstep
3
3
  module VisitWithWaiting
4
- def visit(*args, **kwargs, &block)
5
- super(*args, **kwargs, &block).tap do
6
- # There is a step that changes drivers mid-scenario.
7
- # It works by creating a new Capybara session and re-visits the
8
- # URL from the previous session. If this happens before a URL is ever
9
- # loaded, it re-visits the URL "data:", which will never "finish"
10
- # initializing.
11
- unless args[0].start_with?('data:')
12
- Capybara::Lockstep.await_initialized
4
+ def visit(*args, &block)
5
+ url = args[0]
6
+ # Some of our apps have a Cucumber step that changes drivers mid-scenario.
7
+ # It works by creating a new Capybara session and re-visits the URL from the
8
+ # previous session. If this happens before a URL is ever loaded,
9
+ # it re-visits the URL "data:", which will never "finish" initializing.
10
+ # Also when opening a new tab via Capybara, the initial URL is about:blank.
11
+ visiting_remote_url = !(url.start_with?('data:') || url.start_with?('about:'))
12
+
13
+ if visiting_remote_url
14
+ # We're about to leave this screen, killing all in-flight requests.
15
+ Capybara::Lockstep.synchronize
16
+ end
17
+
18
+ super(*args, &block).tap do
19
+ if visiting_remote_url
20
+ # puts "After visit: unsynchronizing"
21
+ Capybara::Lockstep.synchronized = false
13
22
  end
14
23
  end
15
24
  end
16
25
  end
26
+ end
27
+ end
28
+
17
29
 
18
- module AwaitIdle
19
- def await_idle(meth)
30
+ Capybara::Session.class_eval do
31
+ prepend Capybara::Lockstep::VisitWithWaiting
32
+ end
33
+
34
+ module Capybara
35
+ module Lockstep
36
+ module UnsychronizeAfter
37
+ def unsychronize_after(meth)
20
38
  mod = Module.new do
21
39
  define_method meth do |*args, &block|
22
40
  super(*args, &block).tap do
23
- Capybara::Lockstep.await_idle
41
+ Capybara::Lockstep.synchronized = false
24
42
  end
25
43
  end
26
44
  end
@@ -30,10 +48,6 @@ module Capybara
30
48
  end
31
49
  end
32
50
 
33
- Capybara::Session.class_eval do
34
- prepend Capybara::Lockstep::VisitWithWaiting
35
- end
36
-
37
51
  # Capybara 3 has driver-specific Node classes which sometimes
38
52
  # super to Capybara::Selenium::Node, but not always.
39
53
  node_classes = [
@@ -52,20 +66,38 @@ end
52
66
 
53
67
  node_classes.each do |node_class|
54
68
  node_class.class_eval do
55
- extend Capybara::Lockstep::AwaitIdle
69
+ extend Capybara::Lockstep::UnsychronizeAfter
56
70
 
57
- await_idle :set
58
- await_idle :select_option
59
- await_idle :unselect_option
60
- await_idle :click
61
- await_idle :right_click
62
- await_idle :double_click
63
- await_idle :send_keys
64
- await_idle :hover
65
- await_idle :drag_to
66
- await_idle :drop
67
- await_idle :scroll_by
68
- await_idle :scroll_to
69
- await_idle :trigger
71
+ unsychronize_after :set
72
+ unsychronize_after :select_option
73
+ unsychronize_after :unselect_option
74
+ unsychronize_after :click
75
+ unsychronize_after :right_click
76
+ unsychronize_after :double_click
77
+ unsychronize_after :send_keys
78
+ unsychronize_after :hover
79
+ unsychronize_after :drag_to
80
+ unsychronize_after :drop
81
+ unsychronize_after :scroll_by
82
+ unsychronize_after :scroll_to
83
+ unsychronize_after :trigger
70
84
  end
71
85
  end
86
+
87
+ module Capybara
88
+ module Lockstep
89
+ module SynchronizeWithCatchUp
90
+ def synchronize(*args, &block)
91
+ # This method is called very frequently by capybara.
92
+ # We use the { lazy } option to only synchronize when we're out of sync.
93
+ Capybara::Lockstep.synchronize(lazy: true)
94
+
95
+ super(*args, &block)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ Capybara::Node::Base.class_eval do
102
+ prepend Capybara::Lockstep::SynchronizeWithCatchUp
103
+ end
@@ -30,6 +30,10 @@ module Capybara
30
30
  @enabled = enabled
31
31
  end
32
32
 
33
+ def disabled?
34
+ !enabled?
35
+ end
36
+
33
37
  private
34
38
 
35
39
  def javascript_driver?
@@ -37,7 +37,7 @@ window.CapybaraLockstep = (function() {
37
37
 
38
38
  if (isIdle()) {
39
39
  idleCallbacks.forEach(function(callback) {
40
- callback('JavaScript has finished')
40
+ callback('Finished waiting for JavaScript')
41
41
  })
42
42
  idleCallbacks = []
43
43
  }
@@ -95,19 +95,6 @@ window.CapybaraLockstep = (function() {
95
95
  startWorkForMicrotask()
96
96
  }
97
97
 
98
- function trackHistory() {
99
- ['popstate'].forEach(function(eventType) {
100
- document.addEventListener(eventType, onHistoryEvent)
101
- })
102
- }
103
-
104
- function onHistoryEvent() {
105
- // After calling history.back() or history.forward() the popstate event will *not*
106
- // fire synchronously. It will also not fire in the next task. Chrome sometimes fires
107
- // it after 10ms, but sometimes it takes longer.
108
- startWorkForTime(100)
109
- }
110
-
111
98
  function trackDynamicScripts() {
112
99
  if (!window.MutationObserver) {
113
100
  return
@@ -116,7 +103,7 @@ window.CapybaraLockstep = (function() {
116
103
  // Dynamic imports or analytics snippets may insert a <script src>
117
104
  // tag that loads and executes additional JavaScript. We want to be isBusy()
118
105
  // until such scripts have loaded or errored.
119
- var observer = new MutationObserver(onMutated)
106
+ var observer = new MutationObserver(onAnyElementChanged)
120
107
  observer.observe(document, { subtree: true, childList: true })
121
108
  }
122
109
 
@@ -139,6 +126,28 @@ window.CapybaraLockstep = (function() {
139
126
  })
140
127
  }
141
128
 
129
+ var INITIALIZING_ATTRIBUTE = 'data-initializing'
130
+
131
+ function trackHydration() {
132
+ // Until we have a body on which we can observe [data-initializing]
133
+ // we consider ourselves busy.
134
+ startWork()
135
+ whenReady(function() {
136
+ stopWork()
137
+ if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
138
+ startWork()
139
+ var observer = new MutationObserver(onInitializingAttributeChanged)
140
+ observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
141
+ }
142
+ })
143
+ }
144
+
145
+ function onInitializingAttributeChanged() {
146
+ if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
147
+ stopWork()
148
+ }
149
+ }
150
+
142
151
  function isRemoteScript(node) {
143
152
  if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
144
153
  var src = node.getAttribute('src')
@@ -155,7 +164,7 @@ window.CapybaraLockstep = (function() {
155
164
  script.addEventListener('error', stopWork)
156
165
  }
157
166
 
158
- function onMutated(changes) {
167
+ function onAnyElementChanged(changes) {
159
168
  changes.forEach(function(change) {
160
169
  change.addedNodes.forEach(function(addedNode) {
161
170
  if (isRemoteScript(addedNode)) {
@@ -168,7 +177,7 @@ window.CapybaraLockstep = (function() {
168
177
  function whenReady(callback) {
169
178
  // Values are "loading", "interactive" and "completed".
170
179
  // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
171
- if (document.readyState != 'loading') {
180
+ if (document.readyState !== 'loading') {
172
181
  callback()
173
182
  } else {
174
183
  document.addEventListener('DOMContentLoaded', callback)
@@ -179,12 +188,12 @@ window.CapybaraLockstep = (function() {
179
188
  trackFetch()
180
189
  trackXHR()
181
190
  trackInteraction()
182
- trackHistory()
183
191
  trackDynamicScripts()
184
192
  trackJQuery()
193
+ trackHydration()
185
194
  }
186
195
 
187
- function awaitIdle(callback) {
196
+ function synchronize(callback) {
188
197
  if (isIdle()) {
189
198
  callback()
190
199
  } else {
@@ -196,7 +205,7 @@ window.CapybaraLockstep = (function() {
196
205
  track: track,
197
206
  startWork: startWork,
198
207
  stopWork: stopWork,
199
- awaitIdle: awaitIdle,
208
+ synchronize: synchronize,
200
209
  isIdle: isIdle,
201
210
  isBusy: isBusy
202
211
  }
@@ -1,102 +1,101 @@
1
1
  module Capybara
2
2
  module Lockstep
3
+ ERROR_SNIPPET_MISSING = 'Cannot synchronize: capybara-lockstep JavaScript snippet is missing'
4
+ ERROR_PAGE_MISSING = 'Cannot synchronize before initial Capybara visit'
5
+ ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
6
+ ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
7
+
3
8
  class << self
4
- include Patiently
5
9
  include Configuration
10
+ include Logging
11
+
12
+ attr_accessor :synchronizing
13
+ alias synchronizing? synchronizing
14
+
15
+ def synchronized?
16
+ value = page.instance_variable_get(:@lockstep_synchronized)
17
+ # We consider a new Capybara session to be synchronized.
18
+ # This will be set to false after our first visit().
19
+ value.nil? ? true : value
20
+ end
21
+
22
+ def synchronized=(value)
23
+ page.instance_variable_set(:@lockstep_synchronized, value)
24
+ end
25
+
26
+ def synchronize(lazy: false)
27
+ if (lazy && synchronized?) || synchronizing? || disabled?
28
+ return
29
+ end
30
+
31
+ synchronize_now
32
+ end
33
+
34
+ private
6
35
 
7
- def await_idle
8
- return unless enabled?
36
+ def synchronize_now
37
+ self.synchronizing = true
38
+ self.synchronized = false
9
39
 
10
- ignoring_alerts do
11
- # evaluate_async_script also times out after Capybara.default_max_wait_time
40
+ log 'Synchronizing'
41
+
42
+ begin
12
43
  with_max_wait_time(timeout) do
13
44
  message_from_js = evaluate_async_script(<<~JS)
14
45
  let done = arguments[0]
15
- if (window.CapybaraLockstep) {
16
- CapybaraLockstep.awaitIdle(done)
46
+ let synchronize = () => {
47
+ if (window.CapybaraLockstep) {
48
+ CapybaraLockstep.synchronize(done)
49
+ } else {
50
+ done(#{ERROR_SNIPPET_MISSING.to_json})
51
+ }
52
+ }
53
+ let protocol = location.protocol
54
+ if (protocol === 'data:' || protocol == 'about:') {
55
+ done(#{ERROR_PAGE_MISSING.to_json})
56
+ } else if (document.readyState === 'complete') {
57
+ synchronize()
17
58
  } else {
18
- done('Cannot synchronize: Capybara::Lockstep was not included in page')
59
+ window.addEventListener('load', synchronize)
19
60
  }
20
61
  JS
21
- log(message_from_js)
22
- end
23
- end
24
- end
25
62
 
26
- def await_initialized
27
- return unless enabled?
28
-
29
- # We're retrying the initialize check every few ms.
30
- # Don't clutter the log with dozens of identical messages.
31
- last_logged_reason = nil
32
-
33
- patiently(timeout) do
34
- if (reason = initialize_reason)
35
- if reason != last_logged_reason
36
- log(reason)
37
- last_logged_reason = reason
63
+ case message_from_js
64
+ when ERROR_PAGE_MISSING
65
+ log(message_from_js)
66
+ when ERROR_SNIPPET_MISSING
67
+ log(message_from_js)
68
+ else
69
+ log message_from_js
70
+ log "Synchronized successfully"
71
+ self.synchronized = true
38
72
  end
39
-
40
- # Raise an exception that will be retried by `patiently`
41
- raise Busy, reason
42
73
  end
74
+ rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
75
+ log ERROR_ALERT_OPEN
76
+ # Don't raise an error, this will happen in an innocent test.
77
+ # We will retry on the next Capybara synchronize call.
78
+ rescue ::Selenium::WebDriver::Error::JavascriptError => e
79
+ # When the URL changes while a script is running, my current selenium-webdriver
80
+ # raises a Selenium::WebDriver::Error::JavascriptError with the message:
81
+ # "javascript error: document unloaded while waiting for result".
82
+ # We will retry on the next Capybara synchronize call, by then we should see
83
+ # the new page.
84
+ if e.message.include?('unload')
85
+ log ERROR_NAVIGATED_AWAY
86
+ else
87
+ unhandled_synchronize_error(e)
88
+ end
89
+ rescue StandardError => e
90
+ unhandled_synchronize_error(e)
91
+ ensure
92
+ self.synchronizing = false
43
93
  end
44
94
  end
45
95
 
46
- def idle?
47
- unless enabled?
48
- return true
49
- end
50
-
51
- result = execute_script(<<~JS)
52
- if (window.CapybaraLockstep) {
53
- return CapybaraLockstep.isIdle()
54
- } else {
55
- return 'Cannot check busy state: Capybara::Lockstep was not included in page'
56
- }
57
- JS
58
-
59
- if result.is_a?(String)
60
- log(result)
61
- # When the snippet is missing we assume that the browser is idle.
62
- # Otherwise we would wait forever.
63
- true
64
- else
65
- result
66
- end
67
- end
68
-
69
- def busy?
70
- !idle?
71
- end
72
-
73
- private
74
-
75
- def initialize_reason
76
- ignoring_alerts do
77
- execute_script(<<~JS)
78
- if (location.href.indexOf('data:') == 0) {
79
- return 'Requesting initial page'
80
- }
81
-
82
- if (document.readyState !== "complete") {
83
- return 'Document is loading'
84
- }
85
-
86
- // The application layouts render a <body data-initializing>.
87
- // The [data-initializing] attribute is removed by an Angular directive or Unpoly compiler (frontend).
88
- // to signal that all elements have been activated.
89
- if (document.querySelector('body[data-initializing]')) {
90
- return 'DOM is being hydrated'
91
- }
92
-
93
- if (window.CapybaraLockstep && CapybaraLockstep.isBusy()) {
94
- return 'JavaScript or AJAX requests are running'
95
- }
96
-
97
- return false
98
- JS
99
- end
96
+ def unhandled_synchronize_error(e)
97
+ log "#{e.class.name} while synchronizing: #{e.message}"
98
+ raise e
100
99
  end
101
100
 
102
101
  def page
@@ -105,12 +104,6 @@ module Capybara
105
104
 
106
105
  delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
107
106
 
108
- def ignoring_alerts(&block)
109
- block.call
110
- rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
111
- # noop
112
- end
113
-
114
107
  def with_max_wait_time(seconds, &block)
115
108
  old_max_wait_time = Capybara.default_max_wait_time
116
109
  Capybara.default_max_wait_time = seconds
@@ -121,17 +114,10 @@ module Capybara
121
114
  end
122
115
  end
123
116
 
124
- def log(message)
125
- if debug? && message.present?
126
- message = "[Capybara::Lockstep] #{message}"
127
- if @debug.respond_to?(:debug)
128
- # If someone set Capybara::Lockstep to a logger, use that
129
- @debug.debug(message)
130
- else
131
- # Otherwise print to STDOUT
132
- puts message
133
- end
134
- end
117
+ def ignoring_alerts(&block)
118
+ block.call
119
+ rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
120
+ # no-op
135
121
  end
136
122
 
137
123
  end
@@ -0,0 +1,18 @@
1
+ module Capybara
2
+ module Lockstep
3
+ module Logging
4
+ def log(message)
5
+ if debug? && message.present?
6
+ message = "[capybara-lockstep] #{message}"
7
+ if @debug.respond_to?(:debug)
8
+ # If someone set Capybara::Lockstep to a logger, use that
9
+ @debug.debug(message)
10
+ else
11
+ # Otherwise print to STDOUT
12
+ puts message
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "0.2.2"
3
+ VERSION = "0.3.3"
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: 0.2.2
4
+ version: 0.3.3
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-03-03 00:00:00.000000000 Z
11
+ date: 2021-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -77,7 +77,7 @@ files:
77
77
  - lib/capybara-lockstep/helper.js
78
78
  - lib/capybara-lockstep/helper.rb
79
79
  - lib/capybara-lockstep/lockstep.rb
80
- - lib/capybara-lockstep/patiently.rb
80
+ - lib/capybara-lockstep/logging.rb
81
81
  - lib/capybara-lockstep/version.rb
82
82
  homepage: https://github.com/makandra/capybara-lockstep
83
83
  licenses:
@@ -1,58 +0,0 @@
1
- module Capybara
2
- module Lockstep
3
- # Ported from https://github.com/makandra/spreewald/blob/master/lib/spreewald_support/tolerance_for_selenium_sync_issues.rb
4
- module Patiently
5
-
6
- RETRY_ERRORS = %w[
7
- Capybara::Lockstep::Busy
8
- Capybara::ElementNotFound
9
- Spec::Expectations::ExpectationNotMetError
10
- RSpec::Expectations::ExpectationNotMetError
11
- Minitest::Assertion
12
- Capybara::Poltergeist::ClickFailed
13
- Capybara::ExpectationNotMet
14
- Selenium::WebDriver::Error::StaleElementReferenceError
15
- Selenium::WebDriver::Error::NoAlertPresentError
16
- Selenium::WebDriver::Error::ElementNotVisibleError
17
- Selenium::WebDriver::Error::NoSuchFrameError
18
- Selenium::WebDriver::Error::NoAlertPresentError
19
- Selenium::WebDriver::Error::JavascriptError
20
- Selenium::WebDriver::Error::UnknownError
21
- Selenium::WebDriver::Error::NoSuchAlertError
22
- ]
23
-
24
- # evaluate_script latency is ~ 0.025s
25
- WAIT_PERIOD = 0.03
26
-
27
- def patiently(timeout = Capybara.default_max_wait_time, &block)
28
- started = monotonic_time
29
- tries = 0
30
- begin
31
- tries += 1
32
- block.call
33
- rescue Exception => e
34
- raise e unless retryable_error?(e)
35
- raise e if (monotonic_time - started > timeout && tries >= 2)
36
- sleep(WAIT_PERIOD)
37
- if monotonic_time == started
38
- raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead"
39
- end
40
- retry
41
- end
42
- end
43
-
44
- private
45
-
46
- def monotonic_time
47
- # We use the system clock (i.e. seconds since boot) to calculate the time,
48
- # because Time.now may be frozen
49
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
- end
51
-
52
- def retryable_error?(e)
53
- RETRY_ERRORS.include?(e.class.name)
54
- end
55
-
56
- end
57
- end
58
- end