capybara-lockstep 2.0.3 → 2.2.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 +3 -3
- data/CHANGELOG.md +16 -1
- data/Gemfile.lock +1 -1
- data/README.md +37 -2
- data/capybara-lockstep.gemspec +1 -1
- data/lib/capybara-lockstep/configuration.rb +1 -1
- data/lib/capybara-lockstep/helper.js +86 -51
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa4a510d3f4745ef60812fdf3ab03f6c3e8ede0c6ea4046cb87364d6664e2a7d
|
4
|
+
data.tar.gz: 78be38bdb5aa4fcb9147394ac3adb11f3112c18539ba53a096c214931be753cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 356df47acf01a2642769be49f54c48736ea030c803a92b3c059c587e5e82bb927fa0b4e5dcf7953060162a46491cf0122c6a2d0debd1f607cbd1b09cc8608ff2
|
7
|
+
data.tar.gz: c5d1189ee309d32197f19765dad9014cc59f13a8d9f6a17287537045cbf02664b480ce9d087e933b3fad1a2c63b4460b5dc5e61e20d3e0b98d76cde9d90eaff3
|
data/.github/workflows/test.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -3,9 +3,24 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
4
4
|
|
5
5
|
|
6
|
+
# 2.2.0
|
7
|
+
|
8
|
+
- We now wait for `<video>` and `<audio>` elements to load their metadata. This addresses a race condition where a media element is inserted into the DOM, but another user action deletes or renames the source before the browser could load the initial metadata frames.
|
9
|
+
- We now wait for `<script type="module">`.
|
10
|
+
- We no longer wait for `<img loading="lazy">` or `<iframe loading="lazy">`. This prevents a deadlock where we would wait forever for an element that defers loading until it is scrolled into the viewport.
|
11
|
+
|
12
|
+
|
13
|
+
# 2.1.0
|
14
|
+
|
15
|
+
- We now synchronize for an additional [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) after `history.pushState()`, `history.replaceState()`, `history.forward()`, `history.back()` and `history.go()`.
|
16
|
+
- We now synchronize for an additional JavaScript task after `popstate` and `hashchange` events.
|
17
|
+
- We now synchronize for an additional JavaScript task when the window is resized.
|
18
|
+
- You can now disable automatic synchronization for the duration of a block: `Capybara::Lockstep.with_mode(:manual) { ... }`.
|
19
|
+
|
20
|
+
|
6
21
|
# 2.0.3
|
7
22
|
|
8
|
-
- Fix a bug where we wouldn't wait for an additional JavaScript task after a tracked event or async job.
|
23
|
+
- Fix a bug where we wouldn't wait for an additional [JavaScript task](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) after a tracked event or async job.
|
9
24
|
- Fix a bug where the `Capybara::Lockstep.wait_tasks` configuration would be ignored.
|
10
25
|
- Fix a bug where the `capybara_lockstep_js` helper (for use without Rails) would not include the current configuration.
|
11
26
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -71,7 +71,8 @@ When capybara-lockstep synchronizes it will:
|
|
71
71
|
- wait for client-side JavaScript to render or hydrate DOM elements.
|
72
72
|
- wait for any pending AJAX requests to finish and their callbacks to be called.
|
73
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
|
+
- waits for dynamically inserted `<img>` or `<iframe>` elements to load (ignoring [lazy-loaded](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#lazy) elements).
|
75
|
+
- waits for dynamically inserted `<audio>` and `<video>` elements to load their [metadata](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
|
75
76
|
|
76
77
|
In summary Capybara can no longer observe or interact with the page while HTTP requests are in flight.
|
77
78
|
This covers most async work that causes flaky tests.
|
@@ -158,7 +159,23 @@ use Capybara::Lockstep::Middleware
|
|
158
159
|
# Other middleware here
|
159
160
|
```
|
160
161
|
|
162
|
+
### Configuring Selenium WebDriver (recommended)
|
161
163
|
|
164
|
+
By default, webdrivers will automatically dismiss any user prompts (like alerts) when trying to perform an action.
|
165
|
+
While capybara-lockstep carefully detects alerts before synchronizing, and will skip interaction with the browser to avoid accidentally dismissing alerts, it can not synchronize around some rare race conditions.
|
166
|
+
|
167
|
+
[We recommend](https://makandracards.com/makandra/617366-how-to-configure-selenium-webdriver-to-not-automatically-close-alerts-or-other-browser-dialogs) you configure your webdriver to not automatically dismiss user prompts by setting the "unhandled prompt behavior" capability to [`ignore`](https://w3c.github.io/webdriver/#dfn-known-prompt-handling-approaches-table). Using "ignore", errors are raised like with the default behavior, but user prompts are kept open.
|
168
|
+
|
169
|
+
For example, the Chrome driver can be configured like this:
|
170
|
+
```ruby
|
171
|
+
Capybara.register_driver(:selenium) do |app|
|
172
|
+
options = Selenium::WebDriver::Chrome::Options.new(
|
173
|
+
unhandled_prompt_behavior: 'ignore',
|
174
|
+
# ...
|
175
|
+
)
|
176
|
+
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
|
177
|
+
end
|
178
|
+
```
|
162
179
|
|
163
180
|
### Verify successful integration
|
164
181
|
|
@@ -349,7 +366,23 @@ ensure
|
|
349
366
|
end
|
350
367
|
```
|
351
368
|
|
352
|
-
|
369
|
+
You can also disable automatic synchronization for the duration of a block:
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
Capybara::Lockstep.with_mode(:manual) do
|
373
|
+
do_unsynchronized_work
|
374
|
+
end
|
375
|
+
```
|
376
|
+
|
377
|
+
In the `:manual` mode you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually:
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
Capybara::Lockstep.with_mode(:manual) do
|
381
|
+
do_some_work
|
382
|
+
Capybara::Lockstep.synchronize
|
383
|
+
do_other_work
|
384
|
+
end
|
385
|
+
```
|
353
386
|
|
354
387
|
To completely disable synchronization, even when `Capybara::Lockstep.synchronize` is called:
|
355
388
|
|
@@ -360,6 +393,8 @@ Capybara::Lockstep.synchronize # will not synchronize
|
|
360
393
|
|
361
394
|
|
362
395
|
|
396
|
+
|
397
|
+
|
363
398
|
## Handling legacy promises
|
364
399
|
|
365
400
|
Legacy promise implementations (like jQuery's `$.Deferred` and AngularJS' `$q`) work using [tasks instead of microtasks](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). Their AJAX implementations (like `$.ajax()` and `$http`) use task-based promises to signal that a request is done.
|
data/capybara-lockstep.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.metadata["source_code_uri"] = spec.homepage
|
16
16
|
|
17
17
|
spec.metadata["bug_tracker_uri"] = "https://github.com/makandra/capybara-lockstep/issues"
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/makandra/capybara-lockstep/blob/
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/makandra/capybara-lockstep/blob/main/CHANGELOG.md"
|
19
19
|
spec.metadata["rubygems_mfa_required"] = 'true'
|
20
20
|
|
21
21
|
# Specify which files should be added to the gem when it is released.
|
@@ -4,14 +4,14 @@ window.CapybaraLockstep = (function() {
|
|
4
4
|
let jobCount
|
5
5
|
let idleCallbacks
|
6
6
|
let finishedWorkTags
|
7
|
-
let
|
7
|
+
let defaultWaitTasks
|
8
8
|
reset()
|
9
9
|
|
10
10
|
function reset() {
|
11
11
|
jobCount = 0
|
12
12
|
idleCallbacks = []
|
13
13
|
finishedWorkTags = []
|
14
|
-
|
14
|
+
defaultWaitTasks = 1
|
15
15
|
debug = false
|
16
16
|
}
|
17
17
|
|
@@ -57,8 +57,9 @@ window.CapybaraLockstep = (function() {
|
|
57
57
|
promise.then(taggedStopWork, taggedStopWork)
|
58
58
|
}
|
59
59
|
|
60
|
-
function stopWork(tag) {
|
61
|
-
|
60
|
+
function stopWork(tag, waitAdditionalTasks = 0) {
|
61
|
+
let effectiveWaitTasks = defaultWaitTasks + waitAdditionalTasks
|
62
|
+
afterWaitTasks(stopWorkNow.bind(this, tag), effectiveWaitTasks)
|
62
63
|
}
|
63
64
|
|
64
65
|
function stopWorkNow(tag) {
|
@@ -92,6 +93,27 @@ window.CapybaraLockstep = (function() {
|
|
92
93
|
}
|
93
94
|
}
|
94
95
|
|
96
|
+
function trackHistory() {
|
97
|
+
// Wait an additional task because some browsers seem to require additional
|
98
|
+
// time before the URL changes.
|
99
|
+
trackEvent(document, 'popstate', 1)
|
100
|
+
trackEvent(document, 'hashchange', 1)
|
101
|
+
|
102
|
+
// API: https://developer.mozilla.org/en-US/docs/Web/API/History
|
103
|
+
for (let method of ['pushState', 'popState', 'forward', 'back', 'go']) {
|
104
|
+
let workTag = `history.${method}()`
|
105
|
+
let nativeImpl = history[method]
|
106
|
+
history[method] = function(...args) {
|
107
|
+
try {
|
108
|
+
startWork(workTag)
|
109
|
+
return nativeImpl.apply(history, args)
|
110
|
+
} finally {
|
111
|
+
stopWork(workTag, 1)
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
95
117
|
function trackXHR() {
|
96
118
|
let oldOpen = XMLHttpRequest.prototype.open
|
97
119
|
let oldSend = XMLHttpRequest.prototype.send
|
@@ -135,7 +157,7 @@ window.CapybaraLockstep = (function() {
|
|
135
157
|
// CapybaraLockstep.track() is called as the first script in the head.
|
136
158
|
// jQuery will be loaded after us, so we wait until DOMContentReady.
|
137
159
|
whenReady(function() {
|
138
|
-
if (!window.jQuery ||
|
160
|
+
if (!window.jQuery || defaultWaitTasks > 0) {
|
139
161
|
return
|
140
162
|
}
|
141
163
|
|
@@ -155,39 +177,47 @@ window.CapybaraLockstep = (function() {
|
|
155
177
|
}
|
156
178
|
|
157
179
|
function isRemoteScript(element) {
|
158
|
-
|
159
|
-
|
160
|
-
let type = element.getAttribute('type')
|
180
|
+
return element.matches('script[src]') && !hasDataSource(element)
|
181
|
+
}
|
161
182
|
|
162
|
-
|
163
|
-
|
183
|
+
function isTrackableImage(element) {
|
184
|
+
return element.matches('img') &&
|
185
|
+
!element.complete &&
|
186
|
+
!hasDataSource(element) &&
|
187
|
+
element.getAttribute('loading') !== 'lazy'
|
188
|
+
}
|
189
|
+
|
190
|
+
function isTrackableIFrame(element) {
|
191
|
+
return element.matches('iframe') &&
|
192
|
+
!hasDataSource(element) &&
|
193
|
+
element.getAttribute('loading') !== 'lazy'
|
164
194
|
}
|
165
195
|
|
166
|
-
function
|
167
|
-
|
168
|
-
|
169
|
-
|
196
|
+
function hasDataSource(element) {
|
197
|
+
// <img> can have <img src> and <img srcset>
|
198
|
+
// <video> can have <video src> or <video><source src>
|
199
|
+
// <audio> can have <audio src> or <audio><source src>
|
200
|
+
return element.matches('[src*="data:"], [srcset*="data:"]') ||
|
201
|
+
!!element.querySelector('source [src*="data:"], source [srcset*="data:"]')
|
202
|
+
}
|
170
203
|
|
171
|
-
|
172
|
-
|
204
|
+
function isTrackableMediaElement(element) {
|
205
|
+
return element.matches('audio, video') &&
|
206
|
+
element.readyState === 0 && // no metadata known
|
207
|
+
!hasDataSource(element) &&
|
208
|
+
element.getAttribute('preload') !== 'none'
|
209
|
+
}
|
173
210
|
|
174
|
-
|
175
|
-
|
211
|
+
function trackRemoteElement(element, condition, workTag) {
|
212
|
+
trackLoadingElement(element, condition, workTag, 'load', 'error')
|
176
213
|
|
177
|
-
return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
|
178
|
-
}
|
179
214
|
}
|
180
215
|
|
181
|
-
function
|
182
|
-
|
183
|
-
let src = element.getAttribute('src')
|
184
|
-
let localSrcPattern = /^data:/
|
185
|
-
let hasLocalSrc = src && localSrcPattern.test(src)
|
186
|
-
return (src && !hasLocalSrc)
|
187
|
-
}
|
216
|
+
function trackMediaElement(element, condition, workTag) {
|
217
|
+
trackLoadingElement(element, condition, workTag, 'loadedmetadata', 'error')
|
188
218
|
}
|
189
219
|
|
190
|
-
function
|
220
|
+
function trackLoadingElement(element, condition, workTag, loadEvent, errorEvent) {
|
191
221
|
if (!condition(element)) {
|
192
222
|
return
|
193
223
|
}
|
@@ -198,8 +228,8 @@ window.CapybaraLockstep = (function() {
|
|
198
228
|
|
199
229
|
let doStop = function() {
|
200
230
|
stopped = true
|
201
|
-
element.removeEventListener(
|
202
|
-
element.removeEventListener(
|
231
|
+
element.removeEventListener(loadEvent, doStop)
|
232
|
+
element.removeEventListener(errorEvent, doStop)
|
203
233
|
stopWork(workTag)
|
204
234
|
}
|
205
235
|
|
@@ -218,11 +248,11 @@ window.CapybaraLockstep = (function() {
|
|
218
248
|
}
|
219
249
|
|
220
250
|
let scheduleCheckCondition = function() {
|
221
|
-
setTimeout(checkCondition,
|
251
|
+
setTimeout(checkCondition, 150)
|
222
252
|
}
|
223
253
|
|
224
|
-
element.addEventListener(
|
225
|
-
element.addEventListener(
|
254
|
+
element.addEventListener(loadEvent, doStop)
|
255
|
+
element.addEventListener(errorEvent, doStop)
|
226
256
|
|
227
257
|
// We periodically check whether we still think the element will
|
228
258
|
// produce a `load` or `error` event.
|
@@ -234,8 +264,9 @@ window.CapybaraLockstep = (function() {
|
|
234
264
|
change.addedNodes.forEach(function(addedNode) {
|
235
265
|
if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
236
266
|
trackRemoteElement(addedNode, isRemoteScript, 'Script')
|
237
|
-
trackRemoteElement(addedNode,
|
238
|
-
trackRemoteElement(addedNode,
|
267
|
+
trackRemoteElement(addedNode, isTrackableImage, 'Image')
|
268
|
+
trackRemoteElement(addedNode, isTrackableIFrame, 'Inline frame')
|
269
|
+
trackMediaElement(addedNode, isTrackableMediaElement, 'Media element')
|
239
270
|
}
|
240
271
|
})
|
241
272
|
})
|
@@ -255,11 +286,11 @@ window.CapybaraLockstep = (function() {
|
|
255
286
|
}
|
256
287
|
}
|
257
288
|
|
258
|
-
function afterWaitTasks(fn,
|
259
|
-
if (
|
289
|
+
function afterWaitTasks(fn, waitTasks = defaultWaitTasks) {
|
290
|
+
if (waitTasks > 0) {
|
260
291
|
// Wait 1 task and recurse
|
261
292
|
setTimeout(function() {
|
262
|
-
afterWaitTasks(fn,
|
293
|
+
afterWaitTasks(fn, waitTasks - 1)
|
263
294
|
})
|
264
295
|
} else {
|
265
296
|
fn()
|
@@ -282,14 +313,16 @@ window.CapybaraLockstep = (function() {
|
|
282
313
|
})
|
283
314
|
}
|
284
315
|
|
285
|
-
function
|
286
|
-
|
316
|
+
function trackEvent(eventTarget, eventType, waitAdditionalTasks = 0) {
|
317
|
+
eventTarget.addEventListener(eventType, function() {
|
287
318
|
// Only litter the log with interaction events if we're actually going
|
288
319
|
// to be busy for at least 1 task.
|
289
|
-
|
320
|
+
let effectiveWaitTasks = defaultWaitTasks + waitAdditionalTasks
|
321
|
+
|
322
|
+
if (effectiveWaitTasks > 0) {
|
290
323
|
let tag = eventType
|
291
324
|
startWork(tag)
|
292
|
-
stopWork(tag)
|
325
|
+
stopWork(tag, waitAdditionalTasks)
|
293
326
|
}
|
294
327
|
})
|
295
328
|
}
|
@@ -297,17 +330,19 @@ window.CapybaraLockstep = (function() {
|
|
297
330
|
function track() {
|
298
331
|
trackOldUnpoly()
|
299
332
|
trackFetch()
|
333
|
+
trackHistory()
|
300
334
|
trackXHR()
|
301
335
|
trackRemoteElements()
|
302
336
|
trackJQuery()
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
337
|
+
trackEvent(document, 'touchstart')
|
338
|
+
trackEvent(document, 'mousedown')
|
339
|
+
trackEvent(document, 'click')
|
340
|
+
trackEvent(document, 'keydown')
|
341
|
+
trackEvent(document, 'focusin')
|
342
|
+
trackEvent(document, 'focusout')
|
343
|
+
trackEvent(document, 'input')
|
344
|
+
trackEvent(document, 'change')
|
345
|
+
trackEvent(window, 'resize', 1)
|
311
346
|
}
|
312
347
|
|
313
348
|
function synchronize(callback) {
|
@@ -327,7 +362,7 @@ window.CapybaraLockstep = (function() {
|
|
327
362
|
synchronize: synchronize,
|
328
363
|
reset: reset,
|
329
364
|
set debug(value) { debug = value },
|
330
|
-
set waitTasks(value) {
|
365
|
+
set waitTasks(value) { defaultWaitTasks = value }
|
331
366
|
}
|
332
367
|
})()
|
333
368
|
|
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: 2.0
|
4
|
+
version: 2.2.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: 2024-
|
11
|
+
date: 2024-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -107,7 +107,7 @@ metadata:
|
|
107
107
|
homepage_uri: https://github.com/makandra/capybara-lockstep
|
108
108
|
source_code_uri: https://github.com/makandra/capybara-lockstep
|
109
109
|
bug_tracker_uri: https://github.com/makandra/capybara-lockstep/issues
|
110
|
-
changelog_uri: https://github.com/makandra/capybara-lockstep/blob/
|
110
|
+
changelog_uri: https://github.com/makandra/capybara-lockstep/blob/main/CHANGELOG.md
|
111
111
|
rubygems_mfa_required: 'true'
|
112
112
|
post_install_message:
|
113
113
|
rdoc_options: []
|