capybara-lockstep 2.0.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []