capybara-lockstep 1.3.1 → 2.0.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: 963aaa637f5f364f53f2a16087accf68035381e260aa4f7b0b2703d9a00a8abb
4
- data.tar.gz: a48cdd51fc2fcf9e6d87d0d692e1448c90a639d36695b38e97d4a1183a44043c
3
+ metadata.gz: a9d5a9ec157f65f4efb9be5ac2b33c5447552eb70e8d118438a7aad763c57492
4
+ data.tar.gz: 2ddde2a3b51079603d8368f0ebbe81d5205d473beeaf301bb6bdd42c80c968d3
5
5
  SHA512:
6
- metadata.gz: 8e2ab4e0227f8f3511e40b82ba0dfe80df61615547c6291db4f0d16bfa097a1863e612d57bd6d16c86bd5e5a4a9cd8be3f6040cdf3138071d487311dcfbb5b1f
7
- data.tar.gz: 0060560ae282df788924891c6fff31cdf27491e79468f6c172154667466f66784bce54920e31d0838b96ad91d84904f8cb859573c390a5a327b8e4c40351d2f5
6
+ metadata.gz: b7d8723fe020460efad2e46555be0cf50d38e7398103e7462b8d0c21ac8260ee312bd8f7987e1a04ca471d6acb167b6b1e21565ef5ba73b19d021711bbb8743a
7
+ data.tar.gz: e364cdf67046d885d2f3b27404e258a37388cba10edb1f1f83f286fbc7023ff73f872558dfb33862e95ff6df2f3516bcd687a78094b27a4468ec86cc55a6032a
@@ -9,7 +9,7 @@ on:
9
9
  - master
10
10
  workflow_dispatch:
11
11
  branches:
12
- - master
12
+ - master
13
13
  jobs:
14
14
  test:
15
15
  runs-on: ubuntu-20.04
@@ -18,8 +18,6 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  include:
21
- - ruby: 2.6.6
22
- gemfile: Gemfile
23
21
  - ruby: 2.7.2
24
22
  gemfile: Gemfile
25
23
  - ruby: 3.2.0
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ /todo.txt
1
2
  /.bundle/
2
3
  /.yardoc
3
4
  /_yardoc/
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@ 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
+
6
+ # 2.0.0
7
+
8
+ This major release detects many additional sources of flaky tests:
9
+
10
+ - We now synchronize before a user interaction. Previously we only synchronized before an observation. This could lead to race conditions when a test chained multiple interactions without [making an observation in between](https://makandracards.com/makandra/47336-fixing-flaky-e2e-tests#section-interleave-actions-and-expectations).
11
+ - We now synchronize after a user interaction (e.g. after a click). Previously we only synchronized before an observation. This could lead to race conditions when a test made assertions without going through Capybara, e.g. by accessing the database or global state variables.
12
+ - When a job ends (e.g. an AJAX request finishes) we now wait for one [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). This gives event listeners more time to schedule new async work.
13
+ - We now wait one JavaScript task after `touchstart`, `mousedown`, `click` and `keydown` events. This gives event listeners more time to schedule async work after a user interaction.
14
+ - You can now [wait while the backend server is busy](https://github.com/makandra/capybara-lockstep/#including-the-middleware-optional), by using `Capybara::Lockstep::Middleware` in your Rails or Rack app. We previously only waited for AJAX requests on the client, but using the middleware addresses some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
15
+ - You can [signal async work from the backend](https://github.com/makandra/capybara-lockstep/#on-the-backend), e.g. for background jobs. Note that you don't need to signal work for the regular request/response cycle, as this is detected automatically.
16
+
17
+ Although we now cover a lot more edge cases, this releases will not slow down your test suite considerably.
18
+
19
+
5
20
  ## 1.3.1 - 2023-10-25
6
21
 
7
22
  Now synchronizes before and after `evaluate_script`.
data/Gemfile CHANGED
@@ -5,17 +5,15 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in capybara-lockstep.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'activesupport', '~> 6.0'
8
9
  gem "rake", "~> 13.0"
9
10
 
10
11
  gem "rspec", "~> 3.0"
11
- gem 'jasmine'
12
+ gem "rspec-wait"
13
+ gem 'sinatra'
12
14
  gem 'thin' # ruby 3 does not include a webserver
13
- gem 'chrome_remote'
14
-
15
+ gem 'puma'
15
16
  gem 'byebug'
16
17
  gem 'gemika', '>= 0.8.1'
17
-
18
- gem 'activesupport', '~> 6.0'
19
-
20
- gem 'capybara', '= 3.36.0' # last version compatible with Ruby < 2.7, which is in our test matrix
21
- gem 'selenium-webdriver', '=4.1.0' # last version compatible with Ruby < 2.7, which is in our test matrix
18
+ gem 'capybara', '>= 3'
19
+ gem 'selenium-webdriver', '>= 4'
data/Gemfile.lock CHANGED
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (1.3.1)
5
- activesupport (>= 3.2)
6
- capybara (>= 2.0)
4
+ capybara-lockstep (2.0.0)
5
+ activesupport (>= 4.2)
6
+ capybara (>= 3.0)
7
7
  ruby2_keywords
8
- selenium-webdriver (>= 3)
8
+ selenium-webdriver (>= 4.0)
9
9
 
10
10
  GEM
11
11
  remote: https://rubygems.org/
@@ -16,10 +16,10 @@ GEM
16
16
  minitest (>= 5.1)
17
17
  tzinfo (~> 2.0)
18
18
  zeitwerk (~> 2.3)
19
- addressable (2.8.1)
19
+ addressable (2.8.5)
20
20
  public_suffix (>= 2.0.2, < 6.0)
21
21
  byebug (11.1.3)
22
- capybara (3.36.0)
22
+ capybara (3.39.2)
23
23
  addressable
24
24
  matrix
25
25
  mini_mime (>= 0.1.3)
@@ -29,36 +29,34 @@ GEM
29
29
  regexp_parser (>= 1.5, < 3.0)
30
30
  xpath (~> 3.2)
31
31
  childprocess (4.1.0)
32
- chrome_remote (0.3.0)
33
- websocket-driver (~> 0.6)
34
32
  concurrent-ruby (1.1.10)
35
- daemons (1.3.1)
33
+ daemons (1.4.1)
36
34
  diff-lcs (1.3)
37
35
  eventmachine (1.2.7)
38
36
  gemika (0.8.1)
39
37
  i18n (1.12.0)
40
38
  concurrent-ruby (~> 1.0)
41
- jasmine (3.6.0)
42
- jasmine-core (~> 3.6.0)
43
- phantomjs
44
- rack (>= 1.2.1)
45
- rake
46
- jasmine-core (3.6.0)
47
39
  matrix (0.4.2)
48
- mini_mime (1.1.2)
49
- mini_portile2 (2.8.1)
40
+ mini_mime (1.1.5)
41
+ mini_portile2 (2.8.5)
50
42
  minitest (5.16.3)
51
- nokogiri (1.13.8)
52
- mini_portile2 (~> 2.8.0)
43
+ mustermann (3.0.0)
44
+ ruby2_keywords (~> 0.0.1)
45
+ nio4r (2.6.1)
46
+ nokogiri (1.15.5)
47
+ mini_portile2 (~> 2.8.2)
53
48
  racc (~> 1.4)
54
- phantomjs (2.1.1.0)
55
- public_suffix (5.0.0)
56
- racc (1.6.0)
57
- rack (2.2.3)
58
- rack-test (2.0.2)
49
+ public_suffix (5.0.4)
50
+ puma (6.4.0)
51
+ nio4r (~> 2.0)
52
+ racc (1.7.3)
53
+ rack (2.2.8)
54
+ rack-protection (3.1.0)
55
+ rack (~> 2.2, >= 2.2.4)
56
+ rack-test (2.1.0)
59
57
  rack (>= 1.3)
60
- rake (13.0.1)
61
- regexp_parser (2.5.0)
58
+ rake (13.1.0)
59
+ regexp_parser (2.8.2)
62
60
  rexml (3.2.5)
63
61
  rspec (3.7.0)
64
62
  rspec-core (~> 3.7.0)
@@ -73,21 +71,26 @@ GEM
73
71
  diff-lcs (>= 1.2.0, < 2.0)
74
72
  rspec-support (~> 3.7.0)
75
73
  rspec-support (3.7.0)
74
+ rspec-wait (0.0.9)
75
+ rspec (>= 3, < 4)
76
76
  ruby2_keywords (0.0.5)
77
77
  rubyzip (2.3.2)
78
78
  selenium-webdriver (4.1.0)
79
79
  childprocess (>= 0.5, < 5.0)
80
80
  rexml (~> 3.2, >= 3.2.5)
81
81
  rubyzip (>= 1.2.2)
82
- thin (1.8.0)
82
+ sinatra (3.1.0)
83
+ mustermann (~> 3.0)
84
+ rack (~> 2.2, >= 2.2.4)
85
+ rack-protection (= 3.1.0)
86
+ tilt (~> 2.0)
87
+ thin (1.8.2)
83
88
  daemons (~> 1.0, >= 1.0.9)
84
89
  eventmachine (~> 1.0, >= 1.0.4)
85
90
  rack (>= 1, < 3)
91
+ tilt (2.3.0)
86
92
  tzinfo (2.0.5)
87
93
  concurrent-ruby (~> 1.0)
88
- websocket-driver (0.7.3)
89
- websocket-extensions (>= 0.1.0)
90
- websocket-extensions (0.1.5)
91
94
  xpath (3.2.0)
92
95
  nokogiri (~> 1.8)
93
96
  zeitwerk (2.6.0)
@@ -98,14 +101,15 @@ PLATFORMS
98
101
  DEPENDENCIES
99
102
  activesupport (~> 6.0)
100
103
  byebug
101
- capybara (= 3.36.0)
104
+ capybara (>= 3)
102
105
  capybara-lockstep!
103
- chrome_remote
104
106
  gemika (>= 0.8.1)
105
- jasmine
107
+ puma
106
108
  rake (~> 13.0)
107
109
  rspec (~> 3.0)
108
- selenium-webdriver (= 4.1.0)
110
+ rspec-wait
111
+ selenium-webdriver (>= 4)
112
+ sinatra
109
113
  thin
110
114
 
111
115
  BUNDLED WITH
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
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
4
 
5
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).
6
+ If you don't care you may **skip to [installation instructions](#installation)**.
7
7
 
8
8
 
9
9
  Why are tests flaky?
@@ -58,8 +58,9 @@ How capybara-lockstep helps
58
58
 
59
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
60
 
61
- capybara-lockstep synchronizes before:
61
+ capybara-lockstep synchronizes when one of the following occurs:
62
62
 
63
+ - Capybara looks up an element
63
64
  - Capybara simulates a user interaction (clicking, typing, etc.)
64
65
  - Capybara visits a new URL
65
66
  - Capybara executes JavaScript
@@ -69,13 +70,24 @@ When capybara-lockstep synchronizes it will:
69
70
  - wait for all document resources to load (images, CSS, fonts, frames).
70
71
  - wait for client-side JavaScript to render or hydrate DOM elements.
71
72
  - wait for any pending AJAX requests to finish and their callbacks to be called.
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).
73
- - capybara-lockstep waits for dynamically inserted `<img>` or `<iframe>` elements to load.
73
+ - wait for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
74
+ - waits for dynamically inserted `<img>` or `<iframe>` elements to load.
74
75
 
75
- In summary Capybara can no longer observe the page while HTTP requests are in flight.
76
+ In summary Capybara can no longer observe or interact with the page while HTTP requests are in flight.
76
77
  This covers most async work that causes flaky tests.
77
78
 
78
- You can also configure capybara-lockstep to [wait for other async work](#signaling-asynchronous-work) that does not involve the network, like animations.
79
+
80
+ ### Limitations
81
+
82
+ Async work not synchronized by capybara-lockstep includes:
83
+
84
+ - Animations
85
+ - Websocket connections
86
+ - Media elements (`<video>`, `<audio>`)
87
+ - Service workers
88
+ - Work scheduled via `setTimeout()` or `setInterval()`.
89
+
90
+ You can configure capybara-lockstep to [wait for additional async work](#signaling-asynchronous-work).
79
91
 
80
92
 
81
93
  Installation
@@ -85,8 +97,8 @@ Installation
85
97
 
86
98
  Check if your application satisfies all requirements for capybara-lockstep:
87
99
 
88
- - Capybara 2 or higher.
89
- - Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/). capybara-lockstep deactivates itself for any other driver.
100
+ - Capybara 2.0 or higher.
101
+ - Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/) 3.0 or higher. capybara-lockstep deactivates itself for any other driver.
90
102
  - 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.
91
103
  - This gem was only tested with Rails, but there's no Rails dependency.
92
104
 
@@ -110,7 +122,7 @@ $ bundle install
110
122
  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).
111
123
 
112
124
 
113
- ### Including the JavaScript snippet
125
+ ### Including the JavaScript snippet (required)
114
126
 
115
127
  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.
116
128
 
@@ -122,11 +134,31 @@ capybara-lockstep requires a JavaScript snippet to be embedded by the applicatio
122
134
 
123
135
  Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
124
136
 
125
- **If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
137
+ **If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript code with `capybara_lockstep_js`.
126
138
 
127
139
  **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.
128
140
 
129
141
 
142
+ ### Including the middleware (optional)
143
+
144
+ This gem provides a Rack middleware to block Capybara while your Rails (or Rack) backend is busy.
145
+
146
+ Using the middleware is optional, as the [JavaScript snippet](#including-the-javascript-snippet-required) already for AJAX requests on the client. However, using the middleware covers some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
147
+
148
+ To include the middleware in a Rails application, add the following line to `config/environments/test.rb`:
149
+
150
+ ```ruby
151
+ config.middleware.insert_before 0, Capybara::Lockstep::Middleware
152
+ ```
153
+
154
+ In a **non-Rails** application you should include the middleware as high up in your middleware stack as possible:
155
+
156
+ ```ruby
157
+ use Capybara::Lockstep::Middleware
158
+ # Other middleware here
159
+ ```
160
+
161
+
130
162
 
131
163
  ### Verify successful integration
132
164
 
@@ -152,16 +184,22 @@ Note that you may see some failures from tests with wrong assertions, which prev
152
184
 
153
185
  ## Signaling asynchronous work
154
186
 
155
- By default capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
187
+ [By default](#how-capybara-lockstep-helps) capybara-lockstep waits until resources have loaded, AJAX requests have finished and their callbacks have been called.
188
+ There are also some [limitations](#limitations).
189
+
190
+ You can configure capybara-lockstep to wait for other async work.
156
191
 
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:
192
+
193
+ ### On the frontend
194
+
195
+ Let's say we have an animation that fades in a new element over 2 seconds. The following will block Capybara while the animation is running:
158
196
 
159
197
  ```js
160
198
  async function fadeIn(element) {
161
- CapybaraLockstep.startWork('Animation')
199
+ CapybaraLockstep?.startWork('Animation')
162
200
  startAnimation(element, 'fade-in')
163
201
  await waitForAnimationEnd(element)
164
- CapybaraLockstep.stopWork('Animation')
202
+ CapybaraLockstep?.stopWork('Animation')
165
203
  }
166
204
  ```
167
205
 
@@ -175,20 +213,24 @@ The string argument is used for logging (when logging is enabled). It does **not
175
213
  You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
176
214
 
177
215
 
178
- ## Note on interacting with the JavaScript API
216
+ ### On the backend
179
217
 
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.
218
+ You don't need to signal work within the regular request/response cycle, as this is detected automatically. You can however signal
219
+ work that happens outside a request, e.g. in a background job or WebSocket handler.
181
220
 
182
- ```js
183
- if (window.CapybaraLockstep) {
184
- // interact with CapybaraLockstep
185
- }
186
- ```
187
-
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:
221
+ The following will block Capybara while a [Sidekiq](https://sidekiq.org/) job is running:
189
222
 
190
- ```js
191
- window.CapybaraLockstep?.startWork('Work')
223
+ ```ruby
224
+ class HardJob
225
+ include Sidekiq::Job
226
+
227
+ def perform(name, count)
228
+ Capybara::Lockstep.start_work('HardJob') if defined?(Capybara::Lockstep)
229
+ # do something
230
+ ensure
231
+ Capybara::Lockstep.stop_work('StopWork') if defined?(Capybara::Lockstep)
232
+ end
233
+ end
192
234
  ```
193
235
 
194
236
 
@@ -335,14 +377,13 @@ It is theoretically possible that your test will observe the browser in that win
335
377
  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:
336
378
 
337
379
  ```ruby
338
- Capybara::Lockstep.wait_tasks = 1
380
+ Capybara::Lockstep.wait_tasks = 2 // default is 1
339
381
  ```
340
382
 
341
383
  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.
342
384
 
343
385
  Waiting additional tasks will have a negative performance impact on your test suite.
344
386
 
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`.
346
387
 
347
388
 
348
389
  ## Running code after synchronization
data/Rakefile CHANGED
@@ -6,8 +6,6 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
- require 'jasmine'
10
- load 'jasmine/tasks/jasmine.rake'
11
9
 
12
10
  begin
13
11
  require 'gemika/tasks'
@@ -28,9 +28,9 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ["lib"]
29
29
 
30
30
  # Uncomment to register a new dependency of your gem
31
- spec.add_dependency "capybara", ">= 2.0"
32
- spec.add_dependency "selenium-webdriver", ">= 3"
33
- spec.add_dependency "activesupport", ">= 3.2"
31
+ spec.add_dependency "capybara", ">= 3.0"
32
+ spec.add_dependency "selenium-webdriver", ">= 4.0"
33
+ spec.add_dependency "activesupport", ">= 4.2"
34
34
  spec.add_dependency "ruby2_keywords"
35
35
 
36
36
  # For more information and examples about making a new gem, checkout our
@@ -2,13 +2,27 @@ require 'ruby2_keywords'
2
2
 
3
3
  module Capybara
4
4
  module Lockstep
5
- module UnsychronizeAfter
6
- def unsychronize_after(meth)
5
+ module SynchronizeMacros
6
+
7
+ def synchronize_before(meth, lazy:)
8
+ mod = Module.new do
9
+ define_method meth do |*args, &block|
10
+ Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
11
+ super(*args, &block)
12
+ end
13
+
14
+ ruby2_keywords meth
15
+ end
16
+
17
+ prepend(mod)
18
+ end
19
+
20
+ def synchronize_after(meth)
7
21
  mod = Module.new do
8
22
  define_method meth do |*args, &block|
9
23
  super(*args, &block)
10
24
  ensure
11
- Lockstep.synchronized = false
25
+ Lockstep.auto_synchronize
12
26
  end
13
27
 
14
28
  ruby2_keywords meth
@@ -16,18 +30,13 @@ module Capybara
16
30
 
17
31
  prepend(mod)
18
32
  end
19
- end
20
- end
21
- end
22
33
 
23
- module Capybara
24
- module Lockstep
25
- module SynchronizeBefore
26
- def synchronize_before(meth, lazy:)
34
+ def unsynchronize_after(meth)
27
35
  mod = Module.new do
28
36
  define_method meth do |*args, &block|
29
- Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
30
37
  super(*args, &block)
38
+ ensure
39
+ Lockstep.unsynchronize
31
40
  end
32
41
 
33
42
  ruby2_keywords meth
@@ -35,32 +44,32 @@ module Capybara
35
44
 
36
45
  prepend(mod)
37
46
  end
47
+
38
48
  end
39
49
  end
40
50
  end
41
51
 
42
52
  Capybara::Session.class_eval do
43
- extend Capybara::Lockstep::SynchronizeBefore
44
- extend Capybara::Lockstep::UnsychronizeAfter
53
+ extend Capybara::Lockstep::SynchronizeMacros
45
54
 
46
55
  synchronize_before :html, lazy: true # wait until running JavaScript has updated the DOM
47
56
 
48
57
  synchronize_before :current_url, lazy: true # wait until running JavaScript has updated the URL
49
58
 
50
59
  synchronize_before :refresh, lazy: false # wait until running JavaScript has updated the URL
51
- unsychronize_after :refresh # new document is no longer synchronized
60
+ unsynchronize_after :refresh # new document is no longer synchronized
52
61
 
53
62
  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
63
+ unsynchronize_after :go_back # new document is no longer synchronized
55
64
 
56
65
  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
66
+ unsynchronize_after :go_forward # new document is no longer synchronized
58
67
 
59
68
  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.
69
+ unsynchronize_after :switch_to_frame # now that we've switched into the new frame, we don't know the document's synchronization state.
61
70
 
62
71
  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.
72
+ unsynchronize_after :switch_to_window # now that we've switched to the new window, we don't know the document's synchronization state.
64
73
  end
65
74
 
66
75
  module Capybara
@@ -88,7 +97,7 @@ module Capybara
88
97
  super(*args, &block).tap do
89
98
  if visiting_real_url
90
99
  # We haven't yet synchronized the new screen.
91
- Lockstep.synchronized = false
100
+ Lockstep.unsynchronize
92
101
  end
93
102
  end
94
103
  end
@@ -128,7 +137,7 @@ module Capybara
128
137
  if !Lockstep.synchronizing?
129
138
  # We haven't yet synchronized with whatever changes the JavaScript
130
139
  # did on the frontend.
131
- Lockstep.synchronized = false
140
+ Lockstep.unsynchronize
132
141
  end
133
142
  end
134
143
 
@@ -167,21 +176,59 @@ end
167
176
 
168
177
  node_classes.each do |node_class|
169
178
  node_class.class_eval do
170
- extend Capybara::Lockstep::UnsychronizeAfter
171
-
172
- unsychronize_after :set
173
- unsychronize_after :select_option
174
- unsychronize_after :unselect_option
175
- unsychronize_after :click
176
- unsychronize_after :right_click
177
- unsychronize_after :double_click
178
- unsychronize_after :send_keys
179
- unsychronize_after :hover
180
- unsychronize_after :drag_to
181
- unsychronize_after :drop
182
- unsychronize_after :scroll_by
183
- unsychronize_after :scroll_to
184
- unsychronize_after :trigger
179
+ extend Capybara::Lockstep::SynchronizeMacros
180
+
181
+ synchronize_before :set, lazy: true
182
+ unsynchronize_after :set
183
+ synchronize_after :set
184
+
185
+ synchronize_before :select_option, lazy: true
186
+ unsynchronize_after :select_option
187
+ synchronize_after :select_option
188
+
189
+ synchronize_before :unselect_option, lazy: true
190
+ unsynchronize_after :unselect_option
191
+ synchronize_after :unselect_option
192
+
193
+ synchronize_before :click, lazy: true
194
+ unsynchronize_after :click
195
+ synchronize_after :click
196
+
197
+ synchronize_before :right_click, lazy: true
198
+ unsynchronize_after :right_click
199
+ synchronize_after :right_click
200
+
201
+ synchronize_before :double_click, lazy: true
202
+ unsynchronize_after :double_click
203
+ synchronize_after :double_click
204
+
205
+ synchronize_before :send_keys, lazy: true
206
+ unsynchronize_after :send_keys
207
+ synchronize_after :send_keys
208
+
209
+ synchronize_before :hover, lazy: true
210
+ unsynchronize_after :hover
211
+ synchronize_after :hover
212
+
213
+ synchronize_before :drag_to, lazy: true
214
+ unsynchronize_after :drag_to
215
+ synchronize_after :drag_to
216
+
217
+ synchronize_before :drop, lazy: true
218
+ unsynchronize_after :drop
219
+ synchronize_after :drop
220
+
221
+ synchronize_before :scroll_by, lazy: true
222
+ unsynchronize_after :scroll_by
223
+ synchronize_after :scroll_by
224
+
225
+ synchronize_before :scroll_to, lazy: true
226
+ unsynchronize_after :scroll_to
227
+ synchronize_after :scroll_to
228
+
229
+ synchronize_before :trigger, lazy: true
230
+ unsynchronize_after :trigger
231
+ synchronize_after :trigger
185
232
  end
186
233
  end
187
234
 
@@ -0,0 +1,133 @@
1
+ module Capybara
2
+ module Lockstep
3
+ class Client
4
+ include Logging
5
+ include PageAccess
6
+
7
+ ERROR_SNIPPET_MISSING = 'Cannot synchronize: capybara-lockstep JavaScript snippet is missing'
8
+ ERROR_PAGE_MISSING = 'Cannot synchronize with empty page'
9
+ ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
10
+ ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
11
+
12
+ SYNCHRONIZED_IVAR = :@lockstep_synchronized_client
13
+
14
+ def synchronized?
15
+ # The synchronized flag is per-session (page == Capybara.current_session).
16
+ # This enables tests that use more than one browser, e.g. to test multi-user interaction:
17
+ # https://makandracards.com/makandra/474480-how-to-make-a-cucumber-test-work-with-multiple-browser-sessions
18
+ #
19
+ # Ideally the synchronized flag would also be per-tab, per-frame and per-document.
20
+ # We haven't found a way to patch this into Capybara, as there does not seem to be
21
+ # a persistent object representing a document. Capybara::Node::Document just seems to
22
+ # be a proxy accessing whatever is the current document. The way we work around this
23
+ # is that we synchronize before switching tabs or frames.
24
+ value = page.instance_variable_get(SYNCHRONIZED_IVAR)
25
+
26
+ # We consider a new Capybara session to be synchronized.
27
+ # This will be set to false after our first visit().
28
+ value.nil? ? true : value
29
+ end
30
+
31
+ def synchronized=(value)
32
+ page.instance_variable_set(SYNCHRONIZED_IVAR, value)
33
+ end
34
+
35
+ def synchronize
36
+ # If synchronization fails below we consider us unsynchronized after.
37
+ self.synchronized = false
38
+
39
+ # Running the synchronization script while an alert is open would close the alert,
40
+ # most likely causing subsequent expectations to fail.
41
+ if alert_present?
42
+ log ERROR_ALERT_OPEN
43
+ # Don't raise an error, this will happen in an innocent test.
44
+ # We will retry on the next Capybara synchronize call.
45
+ return
46
+ end
47
+
48
+ start_time = Util.current_seconds
49
+
50
+ begin
51
+ Util.with_max_wait_time(timeout) do
52
+ message_from_js = evaluate_async_script(<<~JS)
53
+ let done = arguments[0]
54
+ let synchronize = () => {
55
+ if (window.CapybaraLockstep) {
56
+ CapybaraLockstep.synchronize(done)
57
+ } else {
58
+ done(#{ERROR_SNIPPET_MISSING.to_json})
59
+ }
60
+ }
61
+ const emptyDataURL = /^data:[^,]*,?$/
62
+ if (emptyDataURL.test(location.href) || location.protocol === 'about:') {
63
+ done(#{ERROR_PAGE_MISSING.to_json})
64
+ } else if (document.readyState === 'complete') {
65
+ // WebDriver always waits for the `load` event after a visit(),
66
+ // unless a different page load strategy was configured.
67
+ synchronize()
68
+ } else {
69
+ window.addEventListener('load', synchronize)
70
+ }
71
+ JS
72
+
73
+ case message_from_js
74
+ when ERROR_PAGE_MISSING
75
+ log(message_from_js)
76
+ when ERROR_SNIPPET_MISSING
77
+ log(message_from_js)
78
+ else
79
+ log message_from_js
80
+ end_time = Util.current_seconds
81
+ ms_elapsed = ((end_time.to_f - start_time) * 1000).round
82
+ log "Synchronized client successfully [#{ms_elapsed} ms]"
83
+ self.synchronized = true
84
+ end
85
+ end
86
+ rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
87
+ timeout_message = "Could not synchronize client within #{timeout} seconds"
88
+ log timeout_message
89
+ if timeout_with == :error
90
+ raise Timeout, timeout_message
91
+ else
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.
94
+ end
95
+ rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
96
+ log ERROR_ALERT_OPEN
97
+ # Don't raise an error, this will happen in an innocent test.
98
+ # We will retry on the next Capybara synchronize call.
99
+ rescue ::Selenium::WebDriver::Error::JavascriptError => e
100
+ # When the URL changes while a script is running, my current selenium-webdriver
101
+ # raises a Selenium::WebDriver::Error::JavascriptError with the message:
102
+ # "javascript error: document unloaded while waiting for result".
103
+ # We will retry on the next Capybara synchronize call, by then we should see
104
+ # the new page.
105
+ if e.message.include?('unload')
106
+ log ERROR_NAVIGATED_AWAY
107
+ else
108
+ unhandled_synchronize_error(e)
109
+ end
110
+ rescue StandardError => e
111
+ unhandled_synchronize_error(e)
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def unhandled_synchronize_error(e)
118
+ Lockstep.log "#{e.class.name} while synchronizing: #{e.message}"
119
+ raise e
120
+ end
121
+
122
+ def timeout
123
+ Lockstep.timeout
124
+ end
125
+
126
+ def timeout_with
127
+ Lockstep.timeout_with
128
+ end
129
+
130
+ end
131
+ end
132
+ end
133
+
@@ -18,9 +18,13 @@ module Capybara
18
18
  @timeout_with = action&.to_sym
19
19
  end
20
20
 
21
+ def debug
22
+ @debug.nil? ? false : @debug
23
+ end
24
+
21
25
  def debug?
22
26
  # @debug may also be a Logger object, so convert it to a boolean
23
- @debug.nil? ? false : !!@debug
27
+ !!debug
24
28
  end
25
29
 
26
30
  def debug=(value)
@@ -49,6 +53,14 @@ module Capybara
49
53
  @mode = mode&.to_sym
50
54
  end
51
55
 
56
+ def with_mode(temporary_mode, &block)
57
+ old_mode = mode
58
+ self.mode = temporary_mode
59
+ block.call
60
+ ensure
61
+ self.mode = temporary_mode
62
+ end
63
+
52
64
  def enabled=(enabled)
53
65
  case enabled
54
66
  when true
@@ -93,13 +105,11 @@ module Capybara
93
105
  end
94
106
  end
95
107
 
96
- def javascript_driver?
97
- driver.is_a?(Capybara::Selenium::Driver)
98
- end
99
-
100
108
  def send_config_to_browser(js)
109
+ return unless javascript_driver?
110
+
101
111
  begin
102
- with_max_wait_time(2) do
112
+ Util.with_max_wait_time(2) do
103
113
  page.execute_script(<<~JS)
104
114
  if (window.CapybaraLockstep) {
105
115
  #{js}
@@ -11,7 +11,7 @@ window.CapybaraLockstep = (function() {
11
11
  jobCount = 0
12
12
  idleCallbacks = []
13
13
  finishedWorkTags = []
14
- waitTasks = 0
14
+ waitTasks = 1
15
15
  debug = false
16
16
  }
17
17
 
@@ -58,18 +58,7 @@ window.CapybaraLockstep = (function() {
58
58
  }
59
59
 
60
60
  function stopWork(tag) {
61
- let tasksElapsed = 0
62
-
63
- let check = function() {
64
- if (tasksElapsed < waitTasks) {
65
- tasksElapsed++
66
- setTimeout(check)
67
- } else {
68
- stopWorkNow(tag)
69
- }
70
- }
71
-
72
- check()
61
+ afterWaitTasks(stopWorkNow.bind(this, tag))
73
62
  }
74
63
 
75
64
  function stopWorkNow(tag) {
@@ -266,6 +255,14 @@ window.CapybaraLockstep = (function() {
266
255
  }
267
256
  }
268
257
 
258
+ function afterWaitTasks(fn, tasksLeft = waitTasks) {
259
+ if (tasksLeft > 0) {
260
+ afterWaitTasks(fn, tasksLeft - 1)
261
+ } else {
262
+ fn()
263
+ }
264
+ }
265
+
269
266
  function trackOldUnpoly() {
270
267
  // CapybaraLockstep.track() is called as the first script in the head.
271
268
  // Unpoly will be loaded after us, so we wait until DOMContentReady.
@@ -282,12 +279,32 @@ window.CapybaraLockstep = (function() {
282
279
  })
283
280
  }
284
281
 
282
+ function trackInteraction(eventType) {
283
+ document.addEventListener(eventType, function() {
284
+ // Only litter the log with interaction events if we're actually going
285
+ // to be busy for at least 1 task.
286
+ if (waitTasks > 0) {
287
+ let tag = eventType
288
+ startWork(tag)
289
+ stopWork(tag)
290
+ }
291
+ })
292
+ }
293
+
285
294
  function track() {
286
295
  trackOldUnpoly()
287
296
  trackFetch()
288
297
  trackXHR()
289
298
  trackRemoteElements()
290
299
  trackJQuery()
300
+ trackInteraction('touchstart')
301
+ trackInteraction('mousedown')
302
+ trackInteraction('click')
303
+ trackInteraction('keydown')
304
+ trackInteraction('focusin')
305
+ trackInteraction('focusout')
306
+ trackInteraction('input')
307
+ trackInteraction('change')
291
308
  }
292
309
 
293
310
  function synchronize(callback) {
@@ -1,39 +1,31 @@
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 with empty page'
5
- ERROR_ALERT_OPEN = 'Cannot synchronize while an alert is open'
6
- ERROR_NAVIGATED_AWAY = "Browser navigated away while synchronizing"
7
-
8
3
  class << self
9
4
  include Configuration
10
5
  include Logging
6
+ include PageAccess
11
7
 
12
8
  attr_accessor :synchronizing
13
9
  alias synchronizing? synchronizing
14
10
 
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.
25
- value = page.instance_variable_get(:@lockstep_synchronized)
26
-
27
- # We consider a new Capybara session to be synchronized.
28
- # This will be set to false after our first visit().
29
- value.nil? ? true : value
11
+ def unsynchronize
12
+ client.synchronized = false
30
13
  end
31
14
 
32
- def synchronized=(value)
33
- page.instance_variable_set(:@lockstep_synchronized, value)
15
+ # Automatic synchronization from within the capybara-lockstep should always call #auto_synchronize.
16
+ # This only synchronizes IFF in :auto mode, i.e. the user has not explicitly disabled automatic syncing.
17
+ # The :auto mode has nothing to do with the { lazy } option.
18
+ def auto_synchronize(**options)
19
+ if mode == :auto
20
+ synchronize(**options)
21
+ end
34
22
  end
35
23
 
36
- def synchronize(lazy: false, log: nil)
24
+ def synchronize(lazy: false, log: 'Synchronizing')
25
+ if synchronizing? || mode == :off
26
+ return
27
+ end
28
+
37
29
  # The { lazy } option is a performance optimization that will prevent capybara-lockstep
38
30
  # from synchronizing multiple times in expressions like `page.find('.foo').find('.bar')`.
39
31
  # The { lazy } option has nothing todo with :auto mode.
@@ -48,135 +40,38 @@ module Capybara
48
40
  # thinks we're in sync or not. This always makes an execute_script() rountrip, but picks up
49
41
  # non-lazy synchronization so we pick up client-side work that have not been caused
50
42
  # by Capybara commands.
51
- if (lazy && synchronized?) || synchronizing? || mode == :off
52
- return
53
- end
54
-
55
- synchronize_now(log: log)
56
-
57
- run_after_synchronize_callbacks
58
- end
59
-
60
- # Automatic synchronization from within the capybara-lockstep should always call #auto_synchronize.
61
- # This only synchronizes IFF in :auto mode, i.e. the user has not explicitly disabled automatic syncing.
62
- # The :auto mode has nothing to do with the { lazy } option.
63
- def auto_synchronize(**options)
64
- if mode == :auto
65
- synchronize(**options)
66
- end
67
- end
68
-
69
- private
70
-
71
- def synchronize_now(log: 'Synchronizing')
72
- self.synchronizing = true
73
- self.synchronized = false
74
-
75
- self.log(log)
76
-
77
- start_time = current_seconds
43
+ will_synchronize_client = !(lazy && client.synchronized?)
78
44
 
79
45
  begin
80
- with_max_wait_time(timeout) do
81
- message_from_js = evaluate_async_script(<<~JS)
82
- let done = arguments[0]
83
- let synchronize = () => {
84
- if (window.CapybaraLockstep) {
85
- CapybaraLockstep.synchronize(done)
86
- } else {
87
- done(#{ERROR_SNIPPET_MISSING.to_json})
88
- }
89
- }
90
- const emptyDataURL = /^data:[^,]*,?$/
91
- if (emptyDataURL.test(location.href) || location.protocol === 'about:') {
92
- done(#{ERROR_PAGE_MISSING.to_json})
93
- } else if (document.readyState === 'complete') {
94
- // WebDriver always waits for the `load` event after a visit(),
95
- // unless a different page load strategy was configured.
96
- synchronize()
97
- } else {
98
- window.addEventListener('load', synchronize)
99
- }
100
- JS
101
-
102
- case message_from_js
103
- when ERROR_PAGE_MISSING
104
- log(message_from_js)
105
- when ERROR_SNIPPET_MISSING
106
- log(message_from_js)
107
- else
108
- log message_from_js
109
- end_time = current_seconds
110
- ms_elapsed = ((end_time.to_f - start_time) * 1000).round
111
- log "Synchronized successfully [#{ms_elapsed} ms]"
112
- self.synchronized = true
113
- end
114
- end
115
- rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
116
- timeout_message = "Could not synchronize within #{timeout} seconds"
117
- log timeout_message
118
- if timeout_with == :error
119
- raise Timeout, timeout_message
120
- else
121
- # Don't raise an error, this may happen if the server is slow to respond.
122
- # We will retry on the next Capybara synchronize call.
46
+ # Synchronizing the server is free, so we ignore { lazy } and do it every time.
47
+ server.synchronize
48
+
49
+ if will_synchronize_client
50
+ self.log(log)
51
+ self.synchronizing = true
52
+ client.synchronize
53
+ # Synchronizing the server is free, so we ignore { lazy } and do it every time.
54
+ server.synchronize
123
55
  end
124
- rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
125
- log ERROR_ALERT_OPEN
126
- # Don't raise an error, this will happen in an innocent test.
127
- # We will retry on the next Capybara synchronize call.
128
- rescue ::Selenium::WebDriver::Error::JavascriptError => e
129
- # When the URL changes while a script is running, my current selenium-webdriver
130
- # raises a Selenium::WebDriver::Error::JavascriptError with the message:
131
- # "javascript error: document unloaded while waiting for result".
132
- # We will retry on the next Capybara synchronize call, by then we should see
133
- # the new page.
134
- if e.message.include?('unload')
135
- log ERROR_NAVIGATED_AWAY
136
- else
137
- unhandled_synchronize_error(e)
138
- end
139
- rescue StandardError => e
140
- unhandled_synchronize_error(e)
56
+ ensure
57
+ self.synchronizing = false
141
58
  end
142
59
 
143
- ensure
144
- self.synchronizing = false
145
- end
146
-
147
- def unhandled_synchronize_error(e)
148
- log "#{e.class.name} while synchronizing: #{e.message}"
149
- raise e
150
- end
151
-
152
- def page
153
- Capybara.current_session
154
- end
155
-
156
- delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
157
-
158
- def with_max_wait_time(seconds, &block)
159
- old_max_wait_time = Capybara.default_max_wait_time
160
- Capybara.default_max_wait_time = seconds
161
- begin
162
- block.call
163
- ensure
164
- Capybara.default_max_wait_time = old_max_wait_time
60
+ if will_synchronize_client
61
+ run_after_synchronize_callbacks
165
62
  end
166
63
  end
167
64
 
168
- def ignoring_alerts(&block)
169
- block.call
170
- rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
171
- # no-op
65
+ delegate :start_work, :stop_work, to: :server
66
+
67
+ def server
68
+ @server ||= Server.new
172
69
  end
173
70
 
174
- def current_seconds
175
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
71
+ def client
72
+ @client ||= Client.new
176
73
  end
177
74
 
178
75
  end
179
-
180
76
  end
181
77
  end
182
-
@@ -2,11 +2,11 @@ module Capybara
2
2
  module Lockstep
3
3
  module Logging
4
4
  def log(message)
5
- if debug? && message.present?
5
+ if Lockstep.debug? && message.present?
6
6
  message = "[capybara-lockstep] #{message}"
7
- if is_logger?(@debug)
8
- # If someone set Capybara::Lockstep to a logger, use that
9
- @debug.debug(message)
7
+ if is_logger?(Lockstep.debug)
8
+ # If someone set Capybara::Lockstep.debug to a logger, use that
9
+ Lockstep.debug(message)
10
10
  else
11
11
  # Otherwise print to STDOUT
12
12
  puts message
@@ -17,8 +17,8 @@ module Capybara
17
17
  private
18
18
 
19
19
  def is_logger?(object)
20
- @debug.respond_to?(:debug)
20
+ object.respond_to?(:debug)
21
21
  end
22
22
  end
23
23
  end
24
- end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Capybara
2
+ module Lockstep
3
+ class Middleware
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ tag = "Server request for #{env['PATH_INFO'] || 'unknown path'}"
11
+ Lockstep.start_work(tag)
12
+
13
+ begin
14
+ @app.call(env)
15
+ ensure
16
+ Lockstep.stop_work(tag)
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module Capybara
2
+ module Lockstep
3
+ module PageAccess
4
+ def page
5
+ Capybara.current_session
6
+ end
7
+
8
+ delegate :evaluate_script, :evaluate_async_script, :execute_script, :driver, to: :page
9
+
10
+ def javascript_driver?
11
+ driver.is_a?(Capybara::Selenium::Driver)
12
+ end
13
+
14
+ def alert_present?
15
+ # Chrome 54 and/or Chromedriver 2.24 introduced a breaking change on how
16
+ # accessing browser logs work.
17
+ #
18
+ # Apparently, while an alert/confirm is open, Chrome will block any requests
19
+ # to its `getLog` API. This causes Selenium to time out with a `Net::ReadTimeout` error
20
+ page.driver.browser.switch_to.alert
21
+ true
22
+ rescue Capybara::NotSupportedByDriverError, ::Selenium::WebDriver::Error::NoSuchAlertError
23
+ false
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ module Capybara
2
+ module Lockstep
3
+ class Server
4
+ include Logging
5
+
6
+ def initialize
7
+ @job_count = 0
8
+ @job_count_mutex = Mutex.new
9
+ @idle_condition = ConditionVariable.new
10
+ end
11
+
12
+ attr_accessor :job_count
13
+
14
+ def start_work(tag)
15
+ job_count_mutex.synchronize do
16
+ self.job_count += 1
17
+ log("Started server work: #{tag} [#{job_count} server jobs]") if tag
18
+ end
19
+ end
20
+
21
+ def stop_work(tag)
22
+ job_count_mutex.synchronize do
23
+ self.job_count -= 1
24
+ log("Stopped server work: #{tag} [#{job_count} server jobs]") if tag
25
+
26
+ if job_count == 0
27
+ idle_condition.broadcast
28
+ end
29
+ end
30
+ end
31
+
32
+ def synchronize
33
+ job_count_mutex.synchronize do
34
+ if job_count > 0
35
+ idle_condition.wait(job_count_mutex)
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :job_count_mutex, :idle_condition
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ module Capybara
2
+ module Lockstep
3
+ module Util
4
+ class << self
5
+ def with_max_wait_time(seconds, &block)
6
+ old_max_wait_time = Capybara.default_max_wait_time
7
+ Capybara.default_max_wait_time = seconds
8
+ begin
9
+ block.call
10
+ ensure
11
+ Capybara.default_max_wait_time = old_max_wait_time
12
+ end
13
+ end
14
+
15
+ def current_seconds
16
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "1.3.1"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -10,8 +10,13 @@ end
10
10
 
11
11
  require_relative 'capybara-lockstep/version'
12
12
  require_relative 'capybara-lockstep/errors'
13
+ require_relative 'capybara-lockstep/util'
13
14
  require_relative 'capybara-lockstep/configuration'
14
15
  require_relative 'capybara-lockstep/logging'
16
+ require_relative 'capybara-lockstep/page_access'
15
17
  require_relative 'capybara-lockstep/lockstep'
16
18
  require_relative 'capybara-lockstep/capybara_ext'
17
19
  require_relative 'capybara-lockstep/helper'
20
+ require_relative 'capybara-lockstep/server'
21
+ require_relative 'capybara-lockstep/client'
22
+ require_relative 'capybara-lockstep/middleware'
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.3.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-25 00:00:00.000000000 Z
11
+ date: 2023-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '3.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: '3.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: selenium-webdriver
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3'
33
+ version: '4.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3'
40
+ version: '4.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '3.2'
47
+ version: '4.2'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '3.2'
54
+ version: '4.2'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: ruby2_keywords
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -88,12 +88,17 @@ files:
88
88
  - capybara-lockstep.gemspec
89
89
  - lib/capybara-lockstep.rb
90
90
  - lib/capybara-lockstep/capybara_ext.rb
91
+ - lib/capybara-lockstep/client.rb
91
92
  - lib/capybara-lockstep/configuration.rb
92
93
  - lib/capybara-lockstep/errors.rb
93
94
  - lib/capybara-lockstep/helper.js
94
95
  - lib/capybara-lockstep/helper.rb
95
96
  - lib/capybara-lockstep/lockstep.rb
96
97
  - lib/capybara-lockstep/logging.rb
98
+ - lib/capybara-lockstep/middleware.rb
99
+ - lib/capybara-lockstep/page_access.rb
100
+ - lib/capybara-lockstep/server.rb
101
+ - lib/capybara-lockstep/util.rb
97
102
  - lib/capybara-lockstep/version.rb
98
103
  homepage: https://github.com/makandra/capybara-lockstep
99
104
  licenses: