Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize media elements (and more) #16

Merged
merged 5 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).


# 2.2.0

- 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.
- We now wait for `<script type="module">`.
- 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.


# 2.1.0

- 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()`.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
capybara-lockstep (2.1.0)
capybara-lockstep (2.2.0)
activesupport (>= 4.2)
capybara (>= 3.0)
ruby2_keywords
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ When capybara-lockstep synchronizes it will:
- wait for client-side JavaScript to render or hydrate DOM elements.
- wait for any pending AJAX requests to finish and their callbacks to be called.
- 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).
- waits for dynamically inserted `<img>` or `<iframe>` elements to load.
- 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).
- waits for dynamically inserted `<audio>` and `<video>` elements to load their [metadata](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event)

In summary Capybara can no longer observe or interact with the page while HTTP requests are in flight.
This covers most async work that causes flaky tests.
Expand Down
69 changes: 39 additions & 30 deletions lib/capybara-lockstep/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,39 +177,47 @@ window.CapybaraLockstep = (function() {
}

function isRemoteScript(element) {
if (element.tagName === 'SCRIPT') {
let src = element.getAttribute('src')
let type = element.getAttribute('type')
return element.matches('script[src]') && !hasDataSource(element)
}

return src && (!type || /javascript/i.test(type))
}
function isTrackableImage(element) {
return element.matches('img') &&
!element.complete &&
!hasDataSource(element) &&
element.getAttribute('loading') !== 'lazy'
}

function isRemoteImage(element) {
if (element.tagName === 'IMG' && !element.complete) {
let src = element.getAttribute('src')
let srcSet = element.getAttribute('srcset')
function isTrackableIFrame(element) {
return element.matches('iframe') &&
!hasDataSource(element) &&
element.getAttribute('loading') !== 'lazy'
}

function hasDataSource(element) {
// <img> can have <img src> and <img srcset>
// <video> can have <video src> or <video><source src>
// <audio> can have <audio src> or <audio><source src>
return element.matches('[src*="data:"], [srcset*="data:"]') ||
!!element.querySelector('source [src*="data:"], source [srcset*="data:"]')
}

let localSrcPattern = /^data:/
let localSrcSetPattern = /(^|\s)data:/
function isTrackableMediaElement(element) {
return element.matches('audio, video') &&
element.readyState === 0 && // no metadata known
!hasDataSource(element) &&
element.getAttribute('preload') !== 'none'
}

let hasLocalSrc = src && localSrcPattern.test(src)
let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
function trackRemoteElement(element, condition, workTag) {
trackLoadingElement(element, condition, workTag, 'load', 'error')

return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
}
}

function isRemoteInlineFrame(element) {
if (element.tagName === 'IFRAME') {
let src = element.getAttribute('src')
let localSrcPattern = /^data:/
let hasLocalSrc = src && localSrcPattern.test(src)
return (src && !hasLocalSrc)
}
function trackMediaElement(element, condition, workTag) {
trackLoadingElement(element, condition, workTag, 'loadedmetadata', 'error')
}

function trackRemoteElement(element, condition, workTag) {
function trackLoadingElement(element, condition, workTag, loadEvent, errorEvent) {
if (!condition(element)) {
return
}
Expand All @@ -220,8 +228,8 @@ window.CapybaraLockstep = (function() {

let doStop = function() {
stopped = true
element.removeEventListener('load', doStop)
element.removeEventListener('error', doStop)
element.removeEventListener(loadEvent, doStop)
element.removeEventListener(errorEvent, doStop)
stopWork(workTag)
}

Expand All @@ -240,11 +248,11 @@ window.CapybaraLockstep = (function() {
}

let scheduleCheckCondition = function() {
setTimeout(checkCondition, 200)
setTimeout(checkCondition, 150)
}

element.addEventListener('load', doStop)
element.addEventListener('error', doStop)
element.addEventListener(loadEvent, doStop)
element.addEventListener(errorEvent, doStop)

// We periodically check whether we still think the element will
// produce a `load` or `error` event.
Expand All @@ -256,8 +264,9 @@ window.CapybaraLockstep = (function() {
change.addedNodes.forEach(function(addedNode) {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
trackRemoteElement(addedNode, isRemoteScript, 'Script')
trackRemoteElement(addedNode, isRemoteImage, 'Image')
trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
trackRemoteElement(addedNode, isTrackableImage, 'Image')
trackRemoteElement(addedNode, isTrackableIFrame, 'Inline frame')
trackMediaElement(addedNode, isTrackableMediaElement, 'Media element')
}
})
})
Expand Down
2 changes: 1 addition & 1 deletion lib/capybara-lockstep/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Capybara
module Lockstep
VERSION = "2.1.0"
VERSION = "2.2.0"
end
end
Loading
Loading