capybara-lockstep 0.4.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +50 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +69 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -8
- data/README.md +142 -106
- data/Rakefile +8 -0
- data/capybara-lockstep.gemspec +1 -0
- data/lib/capybara-lockstep/capybara_ext.rb +64 -9
- data/lib/capybara-lockstep/configuration.rb +60 -22
- data/lib/capybara-lockstep/errors.rb +1 -1
- data/lib/capybara-lockstep/helper.js +140 -78
- data/lib/capybara-lockstep/helper.rb +12 -3
- data/lib/capybara-lockstep/lockstep.rb +33 -6
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +18 -2
data/Rakefile
CHANGED
@@ -6,3 +6,11 @@ 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
|
+
|
12
|
+
begin
|
13
|
+
require 'gemika/tasks'
|
14
|
+
rescue LoadError
|
15
|
+
puts 'Run `gem install gemika` for additional tasks'
|
16
|
+
end
|
data/capybara-lockstep.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency "capybara", ">= 2.0"
|
28
28
|
spec.add_dependency "selenium-webdriver", ">= 3"
|
29
29
|
spec.add_dependency "activesupport", ">= 3.2"
|
30
|
+
spec.add_dependency "ruby2_keywords"
|
30
31
|
|
31
32
|
# For more information and examples about making a new gem, checkout our
|
32
33
|
# guide at: https://bundler.io/guides/creating_gem.html
|
@@ -1,7 +1,9 @@
|
|
1
|
+
require 'ruby2_keywords'
|
2
|
+
|
1
3
|
module Capybara
|
2
4
|
module Lockstep
|
3
5
|
module VisitWithWaiting
|
4
|
-
def visit(*args, &block)
|
6
|
+
ruby2_keywords def visit(*args, &block)
|
5
7
|
url = args[0]
|
6
8
|
# Some of our apps have a Cucumber step that changes drivers mid-scenario.
|
7
9
|
# It works by creating a new Capybara session and re-visits the URL from the
|
@@ -12,13 +14,18 @@ module Capybara
|
|
12
14
|
|
13
15
|
if visiting_remote_url
|
14
16
|
# We're about to leave this screen, killing all in-flight requests.
|
15
|
-
|
17
|
+
# Give pending form submissions etc. a chance to finish before we tear down
|
18
|
+
# the browser environment.
|
19
|
+
#
|
20
|
+
# We force a non-lazy synchronization so we pick up all client-side changes
|
21
|
+
# that have not been caused by Capybara commands.
|
22
|
+
Lockstep.synchronize(lazy: false)
|
16
23
|
end
|
17
24
|
|
18
25
|
super(*args, &block).tap do
|
19
26
|
if visiting_remote_url
|
20
|
-
#
|
21
|
-
|
27
|
+
# We haven't yet synchronized the new screen.
|
28
|
+
Lockstep.synchronized = false
|
22
29
|
end
|
23
30
|
end
|
24
31
|
end
|
@@ -26,11 +33,57 @@ module Capybara
|
|
26
33
|
end
|
27
34
|
end
|
28
35
|
|
29
|
-
|
30
36
|
Capybara::Session.class_eval do
|
31
37
|
prepend Capybara::Lockstep::VisitWithWaiting
|
32
38
|
end
|
33
39
|
|
40
|
+
module Capybara
|
41
|
+
module Lockstep
|
42
|
+
module SynchronizeAroundScriptMethod
|
43
|
+
|
44
|
+
def synchronize_around_script_method(meth)
|
45
|
+
mod = Module.new do
|
46
|
+
define_method meth do |script, *args, &block|
|
47
|
+
# Synchronization uses execute_script itself, so don't synchronize when
|
48
|
+
# we're already synchronizing.
|
49
|
+
if !Lockstep.synchronizing?
|
50
|
+
# It's generally a good idea to synchronize before a JavaScript wants
|
51
|
+
# to access or observe an earlier state change.
|
52
|
+
#
|
53
|
+
# In case the given script navigates away (with `location.href = url`,
|
54
|
+
# `history.back()`, etc.) we would kill all in-flight requests. For this case
|
55
|
+
# we force a non-lazy synchronization so we pick up all client-side changes
|
56
|
+
# that have not been caused by Capybara commands.
|
57
|
+
script_may_navigate_away = script =~ /\b(location|history)\b/
|
58
|
+
Lockstep.auto_synchronize(lazy: !script_may_navigate_away, log: "Synchronizing before script: #{script}")
|
59
|
+
end
|
60
|
+
|
61
|
+
super(script, *args, &block).tap do
|
62
|
+
if !Lockstep.synchronizing?
|
63
|
+
# We haven't yet synchronized with whatever changes the JavaScript
|
64
|
+
# did on the frontend.
|
65
|
+
Lockstep.synchronized = false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
ruby2_keywords meth
|
70
|
+
end
|
71
|
+
prepend(mod)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Capybara::Session.class_eval do
|
79
|
+
extend Capybara::Lockstep::SynchronizeAroundScriptMethod
|
80
|
+
|
81
|
+
synchronize_around_script_method :execute_script
|
82
|
+
synchronize_around_script_method :evaluate_async_script
|
83
|
+
# Don't synchronize around evaluate_script. It calls execute_script
|
84
|
+
# internally and we don't want to synchronize multiple times.
|
85
|
+
end
|
86
|
+
|
34
87
|
module Capybara
|
35
88
|
module Lockstep
|
36
89
|
module UnsychronizeAfter
|
@@ -38,9 +91,10 @@ module Capybara
|
|
38
91
|
mod = Module.new do
|
39
92
|
define_method meth do |*args, &block|
|
40
93
|
super(*args, &block).tap do
|
41
|
-
|
94
|
+
Lockstep.synchronized = false
|
42
95
|
end
|
43
96
|
end
|
97
|
+
ruby2_keywords meth
|
44
98
|
end
|
45
99
|
prepend(mod)
|
46
100
|
end
|
@@ -87,10 +141,11 @@ end
|
|
87
141
|
module Capybara
|
88
142
|
module Lockstep
|
89
143
|
module SynchronizeWithCatchUp
|
90
|
-
def synchronize(*args, &block)
|
91
|
-
# This method is called
|
144
|
+
ruby2_keywords def synchronize(*args, &block)
|
145
|
+
# This method is called by Capybara before most interactions with
|
146
|
+
# the browser. It is a different method than Capybara::Lockstep.synchronize!
|
92
147
|
# We use the { lazy } option to only synchronize when we're out of sync.
|
93
|
-
Capybara::Lockstep.
|
148
|
+
Capybara::Lockstep.auto_synchronize(lazy: true)
|
94
149
|
|
95
150
|
super(*args, &block)
|
96
151
|
end
|
@@ -10,48 +10,71 @@ module Capybara
|
|
10
10
|
@timeout = seconds
|
11
11
|
end
|
12
12
|
|
13
|
+
def timeout_with
|
14
|
+
@timeout_with.nil? ? :log : @timeout_with
|
15
|
+
end
|
16
|
+
|
17
|
+
def timeout_with=(action)
|
18
|
+
@timeout_with = action&.to_sym
|
19
|
+
end
|
20
|
+
|
13
21
|
def debug?
|
14
22
|
# @debug may also be a Logger object, so convert it to a boolean
|
15
23
|
@debug.nil? ? false : !!@debug
|
16
24
|
end
|
17
25
|
|
18
|
-
def debug=(
|
19
|
-
@debug =
|
20
|
-
if
|
21
|
-
target_prose = (is_logger?(
|
26
|
+
def debug=(value)
|
27
|
+
@debug = value
|
28
|
+
if value
|
29
|
+
target_prose = (is_logger?(value) ? 'Ruby logger' : 'STDOUT')
|
22
30
|
log "Logging to #{target_prose} and browser console"
|
23
31
|
end
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if (window.CapybaraLockstep) {
|
29
|
-
CapybaraLockstep.setDebug(#{debug.to_json})
|
30
|
-
}
|
31
|
-
JS
|
32
|
-
end
|
33
|
-
rescue StandardError => e
|
34
|
-
log "#{e.class.name} while enabling logs in browser: #{e.message}"
|
35
|
-
# Don't fail. The next page load will include the snippet with debugging enabled.
|
36
|
-
end
|
33
|
+
send_config_to_browser(<<~JS)
|
34
|
+
CapybaraLockstep.debug = #{value.to_json}
|
35
|
+
JS
|
37
36
|
|
38
37
|
@debug
|
39
38
|
end
|
40
39
|
|
41
|
-
def
|
40
|
+
def mode
|
42
41
|
if javascript_driver?
|
43
|
-
@
|
42
|
+
@mode.nil? ? :auto : @mode
|
44
43
|
else
|
45
|
-
|
44
|
+
:off
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
48
|
+
def mode=(mode)
|
49
|
+
@mode = mode&.to_sym
|
50
|
+
end
|
51
|
+
|
49
52
|
def enabled=(enabled)
|
50
|
-
|
53
|
+
case enabled
|
54
|
+
when true
|
55
|
+
log "Setting `Capybara::Lockstep.enabled = true` is deprecated. Set `Capybara::Lockstep.mode = :auto` instead."
|
56
|
+
self.mode = :auto
|
57
|
+
when false
|
58
|
+
log "Setting `Capybara::Lockstep.enabled = false` is deprecated. Set `Capybara::Lockstep.mode = :manual` or `Capybara::Lockstep.mode = :off` instead."
|
59
|
+
self.mode = :manual
|
60
|
+
when nil
|
61
|
+
# Reset to default
|
62
|
+
self.mode = nil
|
63
|
+
end
|
51
64
|
end
|
52
65
|
|
53
|
-
def
|
54
|
-
|
66
|
+
def wait_tasks
|
67
|
+
@wait_tasks
|
68
|
+
end
|
69
|
+
|
70
|
+
def wait_tasks=(value)
|
71
|
+
@wait_tasks = value
|
72
|
+
|
73
|
+
send_config_to_browser(<<~JS)
|
74
|
+
CapybaraLockstep.waitTasks = #{value.to_json}
|
75
|
+
JS
|
76
|
+
|
77
|
+
@wait_tasks
|
55
78
|
end
|
56
79
|
|
57
80
|
private
|
@@ -60,6 +83,21 @@ module Capybara
|
|
60
83
|
driver.is_a?(Capybara::Selenium::Driver)
|
61
84
|
end
|
62
85
|
|
86
|
+
def send_config_to_browser(js)
|
87
|
+
begin
|
88
|
+
with_max_wait_time(2) do
|
89
|
+
page.execute_script(<<~JS)
|
90
|
+
if (window.CapybaraLockstep) {
|
91
|
+
#{js}
|
92
|
+
}
|
93
|
+
JS
|
94
|
+
end
|
95
|
+
rescue StandardError => e
|
96
|
+
log "#{e.class.name} while configuring capybara-lockstep in browser: #{e.message}"
|
97
|
+
# Don't fail. The next page load will include the snippet with the new config.
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
63
101
|
end
|
64
102
|
end
|
65
103
|
end
|
@@ -1,13 +1,23 @@
|
|
1
1
|
window.CapybaraLockstep = (function() {
|
2
|
-
|
3
|
-
let
|
4
|
-
let
|
2
|
+
// State and configuration
|
3
|
+
let debug
|
4
|
+
let jobCount
|
5
|
+
let idleCallbacks
|
6
|
+
let waitTasks
|
7
|
+
reset()
|
8
|
+
|
9
|
+
function reset() {
|
10
|
+
jobCount = 0
|
11
|
+
idleCallbacks = []
|
12
|
+
waitTasks = 0
|
13
|
+
debug = false
|
14
|
+
}
|
5
15
|
|
6
16
|
function isIdle() {
|
7
17
|
// Can't check for document.readyState or body.initializing here,
|
8
18
|
// since the user might navigate away from the page before it finishes
|
9
19
|
// initializing.
|
10
|
-
return
|
20
|
+
return jobCount === 0
|
11
21
|
}
|
12
22
|
|
13
23
|
function isBusy() {
|
@@ -33,34 +43,43 @@ window.CapybaraLockstep = (function() {
|
|
33
43
|
}
|
34
44
|
|
35
45
|
function startWork(tag) {
|
36
|
-
|
46
|
+
jobCount++
|
37
47
|
if (tag) {
|
38
|
-
logNegative('Started work: %s [%d jobs]', tag,
|
48
|
+
logNegative('Started work: %s [%d jobs]', tag, jobCount)
|
39
49
|
}
|
40
50
|
}
|
41
51
|
|
42
52
|
function startWorkUntil(promise, tag) {
|
43
53
|
startWork(tag)
|
44
|
-
|
54
|
+
let taggedStopWork = stopWork.bind(this, tag)
|
55
|
+
promise.then(taggedStopWork, taggedStopWork)
|
45
56
|
}
|
46
57
|
|
47
|
-
function
|
48
|
-
|
49
|
-
|
58
|
+
function stopWork(tag) {
|
59
|
+
let tasksElapsed = 0
|
60
|
+
|
61
|
+
let check = function() {
|
62
|
+
if (tasksElapsed < waitTasks) {
|
63
|
+
tasksElapsed++
|
64
|
+
setTimeout(check)
|
65
|
+
} else {
|
66
|
+
stopWorkNow(tag)
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
check()
|
50
71
|
}
|
51
72
|
|
52
|
-
function
|
53
|
-
|
73
|
+
function stopWorkNow(tag) {
|
74
|
+
jobCount--
|
54
75
|
|
55
76
|
if (tag) {
|
56
|
-
logPositive('Finished work: %s [%d jobs]', tag,
|
77
|
+
logPositive('Finished work: %s [%d jobs]', tag, jobCount)
|
57
78
|
}
|
58
79
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
})
|
63
|
-
idleCallbacks = []
|
80
|
+
let idleCallback
|
81
|
+
while (isIdle() && (idleCallback = idleCallbacks.shift())) {
|
82
|
+
idleCallback('Finished waiting for browser')
|
64
83
|
}
|
65
84
|
}
|
66
85
|
|
@@ -104,47 +123,32 @@ window.CapybaraLockstep = (function() {
|
|
104
123
|
}
|
105
124
|
}
|
106
125
|
|
107
|
-
function
|
108
|
-
// We already override all interaction methods in the Selenium browser nodes, so they
|
109
|
-
// wait for an idle frame afterwards. However a test script might also dispatch synthetic
|
110
|
-
// events with executate_script() to manipulate the browser in ways that are not possible
|
111
|
-
// with the Capybara API. When we observe such an event we wait until the end of the microtask,
|
112
|
-
// assuming any busy action will be queued by then.
|
113
|
-
['click', 'mousedown', 'keydown', 'change', 'input', 'submit', 'focusin', 'focusout', 'scroll'].forEach(function(eventType) {
|
114
|
-
// Use { useCapture: true } so we get the event before another listener
|
115
|
-
// can prevent it from bubbling up to the document.
|
116
|
-
document.addEventListener(eventType, onInteraction, { capture: true, passive: true })
|
117
|
-
})
|
118
|
-
}
|
119
|
-
|
120
|
-
function onInteraction(event) {
|
121
|
-
// We wait until the end of this microtask, assuming that any callback that
|
122
|
-
// would queue an AJAX request or load additional scripts will run by then.
|
123
|
-
startWorkForMicrotask()
|
124
|
-
}
|
125
|
-
|
126
|
-
function trackDynamicScripts() {
|
126
|
+
function trackRemoteElements() {
|
127
127
|
if (!window.MutationObserver) {
|
128
128
|
return
|
129
129
|
}
|
130
130
|
|
131
|
-
// Dynamic imports or analytics snippets may insert a
|
132
|
-
//
|
131
|
+
// Dynamic imports or analytics snippets may insert a script element
|
132
|
+
// that loads and executes additional JavaScript. We want to be isBusy()
|
133
133
|
// until such scripts have loaded or errored.
|
134
134
|
let observer = new MutationObserver(onAnyElementChanged)
|
135
135
|
observer.observe(document, { subtree: true, childList: true })
|
136
136
|
}
|
137
137
|
|
138
138
|
function trackJQuery() {
|
139
|
-
//
|
139
|
+
// CapybaraLockstep.track() is called as the first script in the head.
|
140
|
+
// jQuery will be loaded after us, so we wait until DOMContentReady.
|
140
141
|
whenReady(function() {
|
141
|
-
if (!window.jQuery) {
|
142
|
+
if (!window.jQuery || waitTasks > 0) {
|
142
143
|
return
|
143
144
|
}
|
144
145
|
|
145
146
|
// Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
|
146
147
|
// not resolve in the next microtask but in the next *task* (it makes itself
|
147
148
|
// async using setTimoeut()). Hence we need to wait for it in addition to XHR.
|
149
|
+
//
|
150
|
+
// If user code also uses $.Deferred(), it is also recommended to set
|
151
|
+
// CapybaraLockdown.waitTasks = 1 or higher.
|
148
152
|
let oldAjax = window.jQuery.ajax
|
149
153
|
window.jQuery.ajax = function() {
|
150
154
|
let promise = oldAjax.apply(this, arguments)
|
@@ -154,56 +158,97 @@ window.CapybaraLockstep = (function() {
|
|
154
158
|
})
|
155
159
|
}
|
156
160
|
|
157
|
-
|
161
|
+
function isRemoteScript(element) {
|
162
|
+
if (element.tagName === 'SCRIPT') {
|
163
|
+
let src = element.getAttribute('src')
|
164
|
+
let type = element.getAttribute('type')
|
158
165
|
|
159
|
-
|
160
|
-
|
161
|
-
// we consider ourselves busy.
|
162
|
-
startWork()
|
163
|
-
whenReady(function() {
|
164
|
-
stopWork()
|
165
|
-
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
166
|
-
startWork('Page initialization')
|
167
|
-
let observer = new MutationObserver(onInitializingAttributeChanged)
|
168
|
-
observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
|
169
|
-
}
|
170
|
-
})
|
166
|
+
return src && (!type || /javascript/i.test(type))
|
167
|
+
}
|
171
168
|
}
|
172
169
|
|
173
|
-
function
|
174
|
-
if (!
|
175
|
-
|
170
|
+
function isRemoteImage(element) {
|
171
|
+
if (element.tagName === 'IMG' && !element.complete) {
|
172
|
+
let src = element.getAttribute('src')
|
173
|
+
let srcSet = element.getAttribute('srcset')
|
174
|
+
|
175
|
+
let localSrcPattern = /^data:/
|
176
|
+
let localSrcSetPattern = /(^|\s)data:/
|
177
|
+
|
178
|
+
let hasLocalSrc = src && localSrcPattern.test(src)
|
179
|
+
let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
|
180
|
+
|
181
|
+
return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
|
176
182
|
}
|
177
183
|
}
|
178
184
|
|
179
|
-
function
|
180
|
-
if (
|
181
|
-
let src =
|
182
|
-
let
|
183
|
-
|
184
|
-
return (src &&
|
185
|
+
function isRemoteInlineFrame(element) {
|
186
|
+
if (element.tagName === 'IFRAME') {
|
187
|
+
let src = element.getAttribute('src')
|
188
|
+
let localSrcPattern = /^data:/
|
189
|
+
let hasLocalSrc = src && localSrcPattern.test(src)
|
190
|
+
return (src && !hasLocalSrc)
|
185
191
|
}
|
186
192
|
}
|
187
193
|
|
188
|
-
function
|
189
|
-
|
194
|
+
function trackRemoteElement(element, condition, workTag) {
|
195
|
+
if (!condition(element)) {
|
196
|
+
return
|
197
|
+
}
|
198
|
+
|
199
|
+
let stopped = false
|
200
|
+
|
190
201
|
startWork(workTag)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
202
|
+
|
203
|
+
let doStop = function() {
|
204
|
+
stopped = true
|
205
|
+
element.removeEventListener('load', doStop)
|
206
|
+
element.removeEventListener('error', doStop)
|
207
|
+
stopWork(workTag)
|
208
|
+
}
|
209
|
+
|
210
|
+
let checkCondition = function() {
|
211
|
+
if (stopped) {
|
212
|
+
// A `load` or `error` event has fired.
|
213
|
+
// We can stop here. No need to schedule another check.
|
214
|
+
return
|
215
|
+
} else if (isDetached(element) || !condition(element)) {
|
216
|
+
// If it is detached or if its `[src]` attribute changes to a data: URL
|
217
|
+
// we may never get a `load` or `error` event.
|
218
|
+
doStop()
|
219
|
+
} else {
|
220
|
+
scheduleCheckCondition()
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
let scheduleCheckCondition = function() {
|
225
|
+
setTimeout(checkCondition, 200)
|
226
|
+
}
|
227
|
+
|
228
|
+
element.addEventListener('load', doStop)
|
229
|
+
element.addEventListener('error', doStop)
|
230
|
+
|
231
|
+
// We periodically check whether we still think the element will
|
232
|
+
// produce a `load` or `error` event.
|
233
|
+
scheduleCheckCondition()
|
195
234
|
}
|
196
235
|
|
197
236
|
function onAnyElementChanged(changes) {
|
198
237
|
changes.forEach(function(change) {
|
199
238
|
change.addedNodes.forEach(function(addedNode) {
|
200
|
-
if (
|
201
|
-
|
239
|
+
if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
240
|
+
trackRemoteElement(addedNode, isRemoteScript, 'Script')
|
241
|
+
trackRemoteElement(addedNode, isRemoteImage, 'Image')
|
242
|
+
trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
|
202
243
|
}
|
203
244
|
})
|
204
245
|
})
|
205
246
|
}
|
206
247
|
|
248
|
+
function isDetached(element) {
|
249
|
+
return !document.contains(element)
|
250
|
+
}
|
251
|
+
|
207
252
|
function whenReady(callback) {
|
208
253
|
// Values are "loading", "interactive" and "completed".
|
209
254
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
|
@@ -214,13 +259,28 @@ window.CapybaraLockstep = (function() {
|
|
214
259
|
}
|
215
260
|
}
|
216
261
|
|
262
|
+
function trackOldUnpoly() {
|
263
|
+
// CapybaraLockstep.track() is called as the first script in the head.
|
264
|
+
// Unpoly will be loaded after us, so we wait until DOMContentReady.
|
265
|
+
whenReady(function() {
|
266
|
+
// Unpoly 0.x would wait one task after DOMContentLoaded before booting.
|
267
|
+
// There's a slim chance that Capybara can observe the page before compilers have run.
|
268
|
+
// Unpoly 1.0+ runs compilers on DOMContentLoaded, so there's no issue.
|
269
|
+
if (window.up?.version?.startsWith('0.')) {
|
270
|
+
startWork('Old Unpoly')
|
271
|
+
setTimeout(function () {
|
272
|
+
stopWork('Old Unpoly')
|
273
|
+
})
|
274
|
+
}
|
275
|
+
})
|
276
|
+
}
|
277
|
+
|
217
278
|
function track() {
|
279
|
+
trackOldUnpoly()
|
218
280
|
trackFetch()
|
219
281
|
trackXHR()
|
220
|
-
|
221
|
-
trackDynamicScripts()
|
282
|
+
trackRemoteElements()
|
222
283
|
trackJQuery()
|
223
|
-
trackHydration()
|
224
284
|
}
|
225
285
|
|
226
286
|
function synchronize(callback) {
|
@@ -233,12 +293,14 @@ window.CapybaraLockstep = (function() {
|
|
233
293
|
|
234
294
|
return {
|
235
295
|
track: track,
|
296
|
+
isBusy: isBusy,
|
297
|
+
isIdle: isIdle,
|
236
298
|
startWork: startWork,
|
237
299
|
stopWork: stopWork,
|
238
300
|
synchronize: synchronize,
|
239
|
-
|
240
|
-
|
241
|
-
}
|
301
|
+
reset: reset,
|
302
|
+
set debug(value) { debug = value },
|
303
|
+
set waitTasks(value) { waitTasks = value }
|
242
304
|
}
|
243
305
|
})()
|
244
306
|
|