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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc0373be228e0d19b35174656617d256a469fb1f652728eaa031b91a9370aa38
4
- data.tar.gz: 45a3b441340e1cf477564f47af7961799ac0cf37e533a42393c89a08744f1657
3
+ metadata.gz: fa4a510d3f4745ef60812fdf3ab03f6c3e8ede0c6ea4046cb87364d6664e2a7d
4
+ data.tar.gz: 78be38bdb5aa4fcb9147394ac3adb11f3112c18539ba53a096c214931be753cb
5
5
  SHA512:
6
- metadata.gz: b3c3f17a3a6b1e32b43ee86106294b9351bc312234070f4308e93dce8dfeae4198831f04c50e20ca37fc533ecbf1f77b51fe11e24c1336f2866018380052e07a
7
- data.tar.gz: 64b39c510daba699db4c0204caa5004e8d367c3e6208a9b2d0a7912925e4dd7c5223e3caf516633310c7f41dbf524af0520715c5051c1d0ead3f8494a0bb0a75
6
+ metadata.gz: 356df47acf01a2642769be49f54c48736ea030c803a92b3c059c587e5e82bb927fa0b4e5dcf7953060162a46491cf0122c6a2d0debd1f607cbd1b09cc8608ff2
7
+ data.tar.gz: c5d1189ee309d32197f19765dad9014cc59f13a8d9f6a17287537045cbf02664b480ce9d087e933b3fad1a2c63b4460b5dc5e61e20d3e0b98d76cde9d90eaff3
@@ -3,13 +3,13 @@ name: Tests
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - master
6
+ - main
7
7
  pull_request:
8
8
  branches:
9
- - master
9
+ - main
10
10
  workflow_dispatch:
11
11
  branches:
12
- - master
12
+ - main
13
13
  jobs:
14
14
  test:
15
15
  runs-on: ubuntu-20.04
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (2.0.3)
4
+ capybara-lockstep (2.2.0)
5
5
  activesupport (>= 4.2)
6
6
  capybara (>= 3.0)
7
7
  ruby2_keywords
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
- In the `:manual` mode you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
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.
@@ -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/master/CHANGELOG.md"
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.
@@ -58,7 +58,7 @@ module Capybara
58
58
  self.mode = temporary_mode
59
59
  block.call
60
60
  ensure
61
- self.mode = temporary_mode
61
+ self.mode = old_mode
62
62
  end
63
63
 
64
64
  def enabled=(enabled)
@@ -4,14 +4,14 @@ window.CapybaraLockstep = (function() {
4
4
  let jobCount
5
5
  let idleCallbacks
6
6
  let finishedWorkTags
7
- let waitTasks
7
+ let defaultWaitTasks
8
8
  reset()
9
9
 
10
10
  function reset() {
11
11
  jobCount = 0
12
12
  idleCallbacks = []
13
13
  finishedWorkTags = []
14
- waitTasks = 1
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
- afterWaitTasks(stopWorkNow.bind(this, tag))
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 || waitTasks > 0) {
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
- if (element.tagName === 'SCRIPT') {
159
- let src = element.getAttribute('src')
160
- let type = element.getAttribute('type')
180
+ return element.matches('script[src]') && !hasDataSource(element)
181
+ }
161
182
 
162
- return src && (!type || /javascript/i.test(type))
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 isRemoteImage(element) {
167
- if (element.tagName === 'IMG' && !element.complete) {
168
- let src = element.getAttribute('src')
169
- let srcSet = element.getAttribute('srcset')
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
- let localSrcPattern = /^data:/
172
- let localSrcSetPattern = /(^|\s)data:/
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
- let hasLocalSrc = src && localSrcPattern.test(src)
175
- let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
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 isRemoteInlineFrame(element) {
182
- if (element.tagName === 'IFRAME') {
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 trackRemoteElement(element, condition, workTag) {
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('load', doStop)
202
- element.removeEventListener('error', doStop)
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, 200)
251
+ setTimeout(checkCondition, 150)
222
252
  }
223
253
 
224
- element.addEventListener('load', doStop)
225
- element.addEventListener('error', doStop)
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, isRemoteImage, 'Image')
238
- trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
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, tasksLeft = waitTasks) {
259
- if (tasksLeft > 0) {
289
+ function afterWaitTasks(fn, waitTasks = defaultWaitTasks) {
290
+ if (waitTasks > 0) {
260
291
  // Wait 1 task and recurse
261
292
  setTimeout(function() {
262
- afterWaitTasks(fn, tasksLeft - 1)
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 trackInteraction(eventType) {
286
- document.addEventListener(eventType, function() {
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
- if (waitTasks > 0) {
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
- trackInteraction('touchstart')
304
- trackInteraction('mousedown')
305
- trackInteraction('click')
306
- trackInteraction('keydown')
307
- trackInteraction('focusin')
308
- trackInteraction('focusout')
309
- trackInteraction('input')
310
- trackInteraction('change')
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) { waitTasks = value }
365
+ set waitTasks(value) { defaultWaitTasks = value }
331
366
  }
332
367
  })()
333
368
 
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "2.0.3"
3
+ VERSION = "2.2.0"
4
4
  end
5
5
  end
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.3
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-01-08 00:00:00.000000000 Z
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/master/CHANGELOG.md
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: []