capybara-lockstep 1.3.1 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/lib/capybara-lockstep/capybara_ext.rb +82 -35
- data/lib/capybara-lockstep/client.rb +133 -0
- data/lib/capybara-lockstep/configuration.rb +8 -6
- data/lib/capybara-lockstep/helper.js +26 -13
- data/lib/capybara-lockstep/lockstep.rb +35 -140
- data/lib/capybara-lockstep/logging.rb +6 -6
- data/lib/capybara-lockstep/middleware.rb +22 -0
- data/lib/capybara-lockstep/page_access.rb +28 -0
- data/lib/capybara-lockstep/server.rb +46 -0
- data/lib/capybara-lockstep/util.rb +21 -0
- data/lib/capybara-lockstep/version.rb +1 -1
- data/lib/capybara-lockstep.rb +5 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f264c637e6de9b4a6ab18e1e33ebf3c81fe46a9a20173b5c4c8097f5be4d8ae6
|
4
|
+
data.tar.gz: f48886c96cbdfe898d5245a85955b76a5caee1c64662e4e12823427b7cb6c883
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c959856af42047611c8c90b4b9c2de6a0a201ba9bff3f5ae9f2bc0a1e32026999283253c6d7ace66305364c57550dc20b5ee131251dcbf80cec707b47994bb30
|
7
|
+
data.tar.gz: be4f33cb2e453d1bf830a75d70a55ffdb43c243ccf0f9d0513bf578b88d0aec6fca135cb4c16f6f13096edd52d44c190794f480ba35140ec965c32e09ddafa6f
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,17 @@ 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-rc1
|
7
|
+
|
8
|
+
- 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).
|
9
|
+
- 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 to Capybara, e.g. by accessing the database or global state variables.
|
10
|
+
- 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.
|
11
|
+
- 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.
|
12
|
+
- You can now wait while the backend server is busy, 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.
|
13
|
+
- You can signal async work from 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.
|
14
|
+
|
15
|
+
|
5
16
|
## 1.3.1 - 2023-10-25
|
6
17
|
|
7
18
|
Now synchronizes before and after `evaluate_script`.
|
data/Gemfile.lock
CHANGED
@@ -2,13 +2,27 @@ require 'ruby2_keywords'
|
|
2
2
|
|
3
3
|
module Capybara
|
4
4
|
module Lockstep
|
5
|
-
module
|
6
|
-
|
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.
|
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
|
-
|
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::
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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::
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
27
|
+
!!debug
|
24
28
|
end
|
25
29
|
|
26
30
|
def debug=(value)
|
@@ -93,13 +97,11 @@ module Capybara
|
|
93
97
|
end
|
94
98
|
end
|
95
99
|
|
96
|
-
def javascript_driver?
|
97
|
-
driver.is_a?(Capybara::Selenium::Driver)
|
98
|
-
end
|
99
|
-
|
100
100
|
def send_config_to_browser(js)
|
101
|
+
return unless javascript_driver?
|
102
|
+
|
101
103
|
begin
|
102
|
-
with_max_wait_time(2) do
|
104
|
+
Util.with_max_wait_time(2) do
|
103
105
|
page.execute_script(<<~JS)
|
104
106
|
if (window.CapybaraLockstep) {
|
105
107
|
#{js}
|
@@ -11,7 +11,7 @@ window.CapybaraLockstep = (function() {
|
|
11
11
|
jobCount = 0
|
12
12
|
idleCallbacks = []
|
13
13
|
finishedWorkTags = []
|
14
|
-
waitTasks =
|
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
|
-
|
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,28 @@ 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 + ' listeners'
|
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')
|
291
304
|
}
|
292
305
|
|
293
306
|
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
|
16
|
-
|
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
|
-
|
33
|
-
|
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:
|
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
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
125
|
-
|
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
|
-
|
144
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
65
|
+
delegate :start_work, :stop_work, to: :server
|
66
|
+
|
67
|
+
def server
|
68
|
+
@server ||= Server.new
|
172
69
|
end
|
173
70
|
|
174
|
-
def
|
175
|
-
|
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?(
|
8
|
-
# If someone set Capybara::Lockstep to a logger, use that
|
9
|
-
|
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
|
-
|
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
|
data/lib/capybara-lockstep.rb
CHANGED
@@ -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:
|
4
|
+
version: 2.0.0.rc1
|
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-
|
11
|
+
date: 2023-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -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:
|
@@ -115,9 +120,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
120
|
version: 2.4.0
|
116
121
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
122
|
requirements:
|
118
|
-
- - "
|
123
|
+
- - ">"
|
119
124
|
- !ruby/object:Gem::Version
|
120
|
-
version:
|
125
|
+
version: 1.3.1
|
121
126
|
requirements: []
|
122
127
|
rubygems_version: 3.4.3
|
123
128
|
signing_key:
|