capybara-lockstep 0.3.2 → 0.7.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 +36 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +34 -7
- data/README.md +109 -56
- data/Rakefile +8 -0
- data/capybara-lockstep.gemspec +1 -0
- data/lib/capybara-lockstep/capybara_ext.rb +62 -7
- data/lib/capybara-lockstep/configuration.rb +44 -4
- data/lib/capybara-lockstep/helper.js +180 -72
- data/lib/capybara-lockstep/helper.rb +16 -1
- data/lib/capybara-lockstep/lockstep.rb +46 -16
- data/lib/capybara-lockstep/logging.rb +8 -2
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +19 -3
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,58 @@ 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.log "Synchronizing before script: #{script}"
|
59
|
+
Lockstep.synchronize(lazy: !script_may_navigate_away)
|
60
|
+
end
|
61
|
+
|
62
|
+
super(script, *args, &block).tap do
|
63
|
+
if !Lockstep.synchronizing?
|
64
|
+
# We haven't yet synchronized with whatever changes the JavaScript
|
65
|
+
# did on the frontend.
|
66
|
+
Lockstep.synchronized = false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
ruby2_keywords meth
|
71
|
+
end
|
72
|
+
prepend(mod)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Capybara::Session.class_eval do
|
80
|
+
extend Capybara::Lockstep::SynchronizeAroundScriptMethod
|
81
|
+
|
82
|
+
synchronize_around_script_method :execute_script
|
83
|
+
synchronize_around_script_method :evaluate_async_script
|
84
|
+
# Don't synchronize around evaluate_script. It calls execute_script
|
85
|
+
# internally and we don't want to synchronize multiple times.
|
86
|
+
end
|
87
|
+
|
34
88
|
module Capybara
|
35
89
|
module Lockstep
|
36
90
|
module UnsychronizeAfter
|
@@ -38,9 +92,10 @@ module Capybara
|
|
38
92
|
mod = Module.new do
|
39
93
|
define_method meth do |*args, &block|
|
40
94
|
super(*args, &block).tap do
|
41
|
-
|
95
|
+
Lockstep.synchronized = false
|
42
96
|
end
|
43
97
|
end
|
98
|
+
ruby2_keywords meth
|
44
99
|
end
|
45
100
|
prepend(mod)
|
46
101
|
end
|
@@ -87,7 +142,7 @@ end
|
|
87
142
|
module Capybara
|
88
143
|
module Lockstep
|
89
144
|
module SynchronizeWithCatchUp
|
90
|
-
def synchronize(*args, &block)
|
145
|
+
ruby2_keywords def synchronize(*args, &block)
|
91
146
|
# This method is called very frequently by capybara.
|
92
147
|
# We use the { lazy } option to only synchronize when we're out of sync.
|
93
148
|
Capybara::Lockstep.synchronize(lazy: true)
|
@@ -3,7 +3,7 @@ module Capybara
|
|
3
3
|
module Configuration
|
4
4
|
|
5
5
|
def timeout
|
6
|
-
@timeout
|
6
|
+
@timeout.nil? ? Capybara.default_max_wait_time : @timeout
|
7
7
|
end
|
8
8
|
|
9
9
|
def timeout=(seconds)
|
@@ -11,11 +11,22 @@ module Capybara
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def debug?
|
14
|
-
@debug
|
14
|
+
# @debug may also be a Logger object, so convert it to a boolean
|
15
|
+
@debug.nil? ? false : !!@debug
|
15
16
|
end
|
16
17
|
|
17
|
-
def debug=(
|
18
|
-
@debug =
|
18
|
+
def debug=(value)
|
19
|
+
@debug = value
|
20
|
+
if value
|
21
|
+
target_prose = (is_logger?(value) ? 'Ruby logger' : 'STDOUT')
|
22
|
+
log "Logging to #{target_prose} and browser console"
|
23
|
+
end
|
24
|
+
|
25
|
+
send_config_to_browser(<<~JS)
|
26
|
+
CapybaraLockstep.debug = #{value.to_json}
|
27
|
+
JS
|
28
|
+
|
29
|
+
@debug
|
19
30
|
end
|
20
31
|
|
21
32
|
def enabled?
|
@@ -30,6 +41,20 @@ module Capybara
|
|
30
41
|
@enabled = enabled
|
31
42
|
end
|
32
43
|
|
44
|
+
def wait_tasks
|
45
|
+
@wait_tasks
|
46
|
+
end
|
47
|
+
|
48
|
+
def wait_tasks=(value)
|
49
|
+
@wait_tasks = value
|
50
|
+
|
51
|
+
send_config_to_browser(<<~JS)
|
52
|
+
CapybaraLockstep.waitTasks = #{value.to_json}
|
53
|
+
JS
|
54
|
+
|
55
|
+
@wait_tasks
|
56
|
+
end
|
57
|
+
|
33
58
|
def disabled?
|
34
59
|
!enabled?
|
35
60
|
end
|
@@ -40,6 +65,21 @@ module Capybara
|
|
40
65
|
driver.is_a?(Capybara::Selenium::Driver)
|
41
66
|
end
|
42
67
|
|
68
|
+
def send_config_to_browser(js)
|
69
|
+
begin
|
70
|
+
with_max_wait_time(2) do
|
71
|
+
page.execute_script(<<~JS)
|
72
|
+
if (window.CapybaraLockstep) {
|
73
|
+
#{js}
|
74
|
+
}
|
75
|
+
JS
|
76
|
+
end
|
77
|
+
rescue StandardError => e
|
78
|
+
log "#{e.class.name} while configuring capybara-lockstep in browser: #{e.message}"
|
79
|
+
# Don't fail. The next page load will include the snippet with the new config.
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
43
83
|
end
|
44
84
|
end
|
45
85
|
end
|
@@ -1,45 +1,85 @@
|
|
1
1
|
window.CapybaraLockstep = (function() {
|
2
|
-
|
3
|
-
|
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
|
+
}
|
4
15
|
|
5
16
|
function isIdle() {
|
6
17
|
// Can't check for document.readyState or body.initializing here,
|
7
18
|
// since the user might navigate away from the page before it finishes
|
8
19
|
// initializing.
|
9
|
-
return
|
20
|
+
return jobCount === 0
|
10
21
|
}
|
11
22
|
|
12
23
|
function isBusy() {
|
13
24
|
return !isIdle()
|
14
25
|
}
|
15
26
|
|
16
|
-
function
|
17
|
-
|
27
|
+
function log(...args) {
|
28
|
+
if (debug) {
|
29
|
+
args[0] = '%c[capybara-lockstep] ' + args[0]
|
30
|
+
args.splice(1, 0, 'color: #666666')
|
31
|
+
console.log.apply(console, args)
|
32
|
+
}
|
18
33
|
}
|
19
34
|
|
20
|
-
function
|
21
|
-
|
22
|
-
|
35
|
+
function logPositive(...args) {
|
36
|
+
args[0] = '%c' + args[0]
|
37
|
+
log(args[0], 'color: #117722', ...args.slice(1))
|
23
38
|
}
|
24
39
|
|
25
|
-
function
|
26
|
-
|
27
|
-
|
40
|
+
function logNegative(...args) {
|
41
|
+
args[0] = '%c' + args[0]
|
42
|
+
log(args[0], 'color: #cc3311', ...args.slice(1))
|
28
43
|
}
|
29
44
|
|
30
|
-
function
|
31
|
-
|
32
|
-
|
45
|
+
function startWork(tag) {
|
46
|
+
jobCount++
|
47
|
+
if (tag) {
|
48
|
+
logNegative('Started work: %s [%d jobs]', tag, jobCount)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
function startWorkUntil(promise, tag) {
|
53
|
+
startWork(tag)
|
54
|
+
let taggedStopWork = stopWork.bind(this, tag)
|
55
|
+
promise.then(taggedStopWork, taggedStopWork)
|
33
56
|
}
|
34
57
|
|
35
|
-
function stopWork() {
|
36
|
-
|
58
|
+
function stopWork(tag) {
|
59
|
+
let tasksElapsed = 0
|
37
60
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
61
|
+
let check = function() {
|
62
|
+
if (tasksElapsed < waitTasks) {
|
63
|
+
tasksElapsed++
|
64
|
+
setTimeout(check)
|
65
|
+
} else {
|
66
|
+
stopWorkNow(tag)
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
check()
|
71
|
+
}
|
72
|
+
|
73
|
+
function stopWorkNow(tag) {
|
74
|
+
jobCount--
|
75
|
+
|
76
|
+
if (tag) {
|
77
|
+
logPositive('Finished work: %s [%d jobs]', tag, jobCount)
|
78
|
+
}
|
79
|
+
|
80
|
+
let idleCallback
|
81
|
+
while (isIdle() && (idleCallback = idleCallbacks.shift())) {
|
82
|
+
idleCallback('Finished waiting for browser')
|
43
83
|
}
|
44
84
|
}
|
45
85
|
|
@@ -48,29 +88,36 @@ window.CapybaraLockstep = (function() {
|
|
48
88
|
return
|
49
89
|
}
|
50
90
|
|
51
|
-
|
91
|
+
let oldFetch = window.fetch
|
52
92
|
window.fetch = function() {
|
53
|
-
|
54
|
-
startWorkUntil(promise)
|
93
|
+
let promise = oldFetch.apply(this, arguments)
|
94
|
+
startWorkUntil(promise, 'fetch ' + arguments[0])
|
55
95
|
return promise
|
56
96
|
}
|
57
97
|
}
|
58
98
|
|
59
99
|
function trackXHR() {
|
60
|
-
|
100
|
+
let oldOpen = XMLHttpRequest.prototype.open
|
101
|
+
let oldSend = XMLHttpRequest.prototype.send
|
102
|
+
|
103
|
+
XMLHttpRequest.prototype.open = function() {
|
104
|
+
this.capybaraLockstepURL = arguments[1]
|
105
|
+
return oldOpen.apply(this, arguments)
|
106
|
+
}
|
61
107
|
|
62
108
|
XMLHttpRequest.prototype.send = function() {
|
63
|
-
|
109
|
+
let workTag = 'XHR to '+ this.capybaraLockstepURL
|
110
|
+
startWork(workTag)
|
64
111
|
|
65
112
|
try {
|
66
113
|
this.addEventListener('readystatechange', function(event) {
|
67
|
-
if (this.readyState === 4) { stopWork() }
|
114
|
+
if (this.readyState === 4) { stopWork(workTag) }
|
68
115
|
}.bind(this))
|
69
116
|
return oldSend.apply(this, arguments)
|
70
117
|
} catch (e) {
|
71
118
|
// If we get a sync exception during request dispatch
|
72
119
|
// we assume the request never went out.
|
73
|
-
stopWork()
|
120
|
+
stopWork(workTag)
|
74
121
|
throw e
|
75
122
|
}
|
76
123
|
}
|
@@ -89,26 +136,17 @@ window.CapybaraLockstep = (function() {
|
|
89
136
|
})
|
90
137
|
}
|
91
138
|
|
92
|
-
function onInteraction() {
|
93
|
-
|
94
|
-
//
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
document.addEventListener(eventType, onHistoryEvent)
|
101
|
-
})
|
102
|
-
}
|
103
|
-
|
104
|
-
function onHistoryEvent() {
|
105
|
-
// After calling history.back() or history.forward() the popstate event will *not*
|
106
|
-
// fire synchronously. It will also not fire in the next task. Chrome sometimes fires
|
107
|
-
// it after 10ms, but sometimes it takes longer.
|
108
|
-
startWorkForTime(100)
|
139
|
+
function onInteraction(event) {
|
140
|
+
startWork()
|
141
|
+
// (1) We wait until the end of this microtask, assuming that any callback that
|
142
|
+
// would queue an AJAX request or load additional scripts will run by then.
|
143
|
+
// (2) For performance reasons we don't wait for `waitTasks` here.
|
144
|
+
// Whatever was queued by an event handler should call us again, and then
|
145
|
+
// we do wait for additional tasks.
|
146
|
+
Promise.resolve().then(stopWorkNow)
|
109
147
|
}
|
110
148
|
|
111
|
-
function
|
149
|
+
function trackRemoteElements() {
|
112
150
|
if (!window.MutationObserver) {
|
113
151
|
return
|
114
152
|
}
|
@@ -116,40 +154,43 @@ window.CapybaraLockstep = (function() {
|
|
116
154
|
// Dynamic imports or analytics snippets may insert a <script src>
|
117
155
|
// tag that loads and executes additional JavaScript. We want to be isBusy()
|
118
156
|
// until such scripts have loaded or errored.
|
119
|
-
|
157
|
+
let observer = new MutationObserver(onAnyElementChanged)
|
120
158
|
observer.observe(document, { subtree: true, childList: true })
|
121
159
|
}
|
122
160
|
|
123
161
|
function trackJQuery() {
|
124
162
|
// jQuery may be loaded after us, so we wait until DOMContentReady.
|
125
163
|
whenReady(function() {
|
126
|
-
if (!window.jQuery) {
|
164
|
+
if (!window.jQuery || waitTasks > 0) {
|
127
165
|
return
|
128
166
|
}
|
129
167
|
|
130
168
|
// Although $.ajax() uses XHR internally, it also uses $.Deferred() which does
|
131
169
|
// not resolve in the next microtask but in the next *task* (it makes itself
|
132
170
|
// async using setTimoeut()). Hence we need to wait for it in addition to XHR.
|
133
|
-
|
134
|
-
|
135
|
-
|
171
|
+
//
|
172
|
+
// If user code also uses $.Deferred(), it is also recommended to set
|
173
|
+
// CapybaraLockdown.waitTasks = 1 or higher.
|
174
|
+
let oldAjax = window.jQuery.ajax
|
175
|
+
window.jQuery.ajax = function() {
|
176
|
+
let promise = oldAjax.apply(this, arguments)
|
136
177
|
startWorkUntil(promise)
|
137
178
|
return promise
|
138
179
|
}
|
139
180
|
})
|
140
181
|
}
|
141
182
|
|
142
|
-
|
183
|
+
let INITIALIZING_ATTRIBUTE = 'data-initializing'
|
143
184
|
|
144
185
|
function trackHydration() {
|
145
186
|
// Until we have a body on which we can observe [data-initializing]
|
146
187
|
// we consider ourselves busy.
|
147
188
|
startWork()
|
148
189
|
whenReady(function() {
|
149
|
-
|
190
|
+
stopWorkNow()
|
150
191
|
if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
151
|
-
startWork()
|
152
|
-
|
192
|
+
startWork('Page initialization')
|
193
|
+
let observer = new MutationObserver(onInitializingAttributeChanged)
|
153
194
|
observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
|
154
195
|
}
|
155
196
|
})
|
@@ -157,36 +198,101 @@ window.CapybaraLockstep = (function() {
|
|
157
198
|
|
158
199
|
function onInitializingAttributeChanged() {
|
159
200
|
if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
|
160
|
-
stopWork()
|
201
|
+
stopWork('Page initialization')
|
161
202
|
}
|
162
203
|
}
|
163
204
|
|
164
|
-
function isRemoteScript(
|
165
|
-
if (
|
166
|
-
|
167
|
-
|
205
|
+
function isRemoteScript(element) {
|
206
|
+
if (element.tagName === 'SCRIPT') {
|
207
|
+
let src = element.getAttribute('src')
|
208
|
+
let type = element.getAttribute('type')
|
168
209
|
|
169
|
-
return
|
210
|
+
return src && (!type || /javascript/i.test(type))
|
170
211
|
}
|
171
212
|
}
|
172
213
|
|
173
|
-
function
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
214
|
+
function isRemoteImage(element) {
|
215
|
+
if (element.tagName === 'IMG' && !element.complete) {
|
216
|
+
let src = element.getAttribute('src')
|
217
|
+
let srcSet = element.getAttribute('srcset')
|
218
|
+
|
219
|
+
let localSrcPattern = /^data:/
|
220
|
+
let localSrcSetPattern = /(^|\s)data:/
|
221
|
+
|
222
|
+
let hasLocalSrc = src && localSrcPattern.test(src)
|
223
|
+
let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
|
224
|
+
|
225
|
+
return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
function isRemoteInlineFrame(element) {
|
230
|
+
if (element.tagName === 'IFRAME') {
|
231
|
+
let src = element.getAttribute('src')
|
232
|
+
let localSrcPattern = /^data:/
|
233
|
+
let hasLocalSrc = src && localSrcPattern.test(src)
|
234
|
+
return (src && !hasLocalSrc)
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
function trackRemoteElement(element, condition, workTag) {
|
239
|
+
if (!condition(element)) {
|
240
|
+
return
|
241
|
+
}
|
242
|
+
|
243
|
+
let stopped = false
|
244
|
+
|
245
|
+
startWork(workTag)
|
246
|
+
|
247
|
+
let doStop = function() {
|
248
|
+
stopped = true
|
249
|
+
element.removeEventListener('load', doStop)
|
250
|
+
element.removeEventListener('error', doStop)
|
251
|
+
stopWork(workTag)
|
252
|
+
}
|
253
|
+
|
254
|
+
let checkCondition = function() {
|
255
|
+
if (stopped) {
|
256
|
+
// A `load` or `error` event has fired.
|
257
|
+
// We can stop here. No need to schedule another check.
|
258
|
+
return
|
259
|
+
} else if (isDetached(element) || !condition(element)) {
|
260
|
+
// If it is detached or if its `[src]` attribute changes to a data: URL
|
261
|
+
// we may never get a `load` or `error` event.
|
262
|
+
doStop()
|
263
|
+
} else {
|
264
|
+
scheduleCheckCondition()
|
265
|
+
}
|
266
|
+
}
|
267
|
+
|
268
|
+
let scheduleCheckCondition = function() {
|
269
|
+
setTimeout(checkCondition, 200)
|
270
|
+
}
|
271
|
+
|
272
|
+
element.addEventListener('load', doStop)
|
273
|
+
element.addEventListener('error', doStop)
|
274
|
+
|
275
|
+
// We periodically check whether we still think the element will
|
276
|
+
// produce a `load` or `error` event.
|
277
|
+
scheduleCheckCondition()
|
178
278
|
}
|
179
279
|
|
180
280
|
function onAnyElementChanged(changes) {
|
181
281
|
changes.forEach(function(change) {
|
182
282
|
change.addedNodes.forEach(function(addedNode) {
|
183
|
-
if (
|
184
|
-
|
283
|
+
if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
284
|
+
trackRemoteElement(addedNode, isRemoteScript, 'Script')
|
285
|
+
trackRemoteElement(addedNode, isRemoteImage, 'Image')
|
286
|
+
trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
|
185
287
|
}
|
186
288
|
})
|
187
289
|
})
|
188
290
|
}
|
189
291
|
|
292
|
+
function isDetached(element) {
|
293
|
+
return !document.contains(element)
|
294
|
+
}
|
295
|
+
|
190
296
|
function whenReady(callback) {
|
191
297
|
// Values are "loading", "interactive" and "completed".
|
192
298
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
|
@@ -201,8 +307,7 @@ window.CapybaraLockstep = (function() {
|
|
201
307
|
trackFetch()
|
202
308
|
trackXHR()
|
203
309
|
trackInteraction()
|
204
|
-
|
205
|
-
trackDynamicScripts()
|
310
|
+
trackRemoteElements()
|
206
311
|
trackJQuery()
|
207
312
|
trackHydration()
|
208
313
|
}
|
@@ -217,11 +322,14 @@ window.CapybaraLockstep = (function() {
|
|
217
322
|
|
218
323
|
return {
|
219
324
|
track: track,
|
325
|
+
isBusy: isBusy,
|
326
|
+
isIdle: isIdle,
|
220
327
|
startWork: startWork,
|
221
328
|
stopWork: stopWork,
|
222
329
|
synchronize: synchronize,
|
223
|
-
|
224
|
-
|
330
|
+
reset: reset,
|
331
|
+
set debug(value) { debug = value },
|
332
|
+
set waitTasks(value) { waitTasks = value }
|
225
333
|
}
|
226
334
|
})()
|
227
335
|
|