Skip to content

Commit

Permalink
Don't wait for <img loading="lazy"> or <iframe loading="lazy">
Browse files Browse the repository at this point in the history
  • Loading branch information
triskweline committed Feb 12, 2024
1 parent 84695c6 commit 963300d
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 54 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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.
Expand Down
39 changes: 14 additions & 25 deletions lib/capybara-lockstep/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,34 +177,23 @@ window.CapybaraLockstep = (function() {
}

function isRemoteScript(element) {
return element.matches('script[src]') && !hasLocalSource(element)
return element.matches('script[src]') && !hasDataSource(element)
}

function isRemoteImage(element) {
if (element.tagName === 'IMG' && !element.complete) {
let src = element.getAttribute('src')
let srcSet = element.getAttribute('srcset')

let localSrcPattern = /^data:/
let localSrcSetPattern = /(^|\s)data:/

let hasLocalSrc = src && localSrcPattern.test(src)
let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)

return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
}
function isTrackableImage(element) {
return element.matches('img') &&
!element.complete &&
!hasDataSource(element) &&
element.getAttribute('loading') !== 'lazy'
}

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 isTrackableIFrame(element) {
return element.matches('iframe') &&
!hasDataSource(element) &&
element.getAttribute('loading') !== 'lazy'
}

function hasLocalSource(element) {
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>
Expand All @@ -215,7 +204,7 @@ window.CapybaraLockstep = (function() {
function isTrackableMediaElement(element) {
return element.matches('audio, video') &&
element.readyState === 0 && // no metadata known
!hasLocalSource(element) &&
!hasDataSource(element) &&
element.getAttribute('preload') !== 'none'
}

Expand Down Expand Up @@ -275,8 +264,8 @@ 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
208 changes: 180 additions & 28 deletions spec/features/synchronization_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,200 @@
wait(0.5.seconds).for(command).to be_finished
end

it 'waits until a dynamically inserted image has loaded' do
App.start_html = <<~HTML
<a href="#" onclick="img = document.createElement('img'); img.src = '/next'; document.body.append(img)">label</a>
HTML
describe 'dynamically inserted images' do

wall = Wall.new
App.next_action = -> do
wall.block
send_file_sync('spec/fixtures/image.png', 'image/png')
it 'waits until the has loaded' do
App.start_html = <<~HTML
<a href="#" onclick="
let img = document.createElement('img');
img.src = '/next';
document.body.append(img);
">label</a>
HTML

wall = Wall.new
App.next_action = -> do
wall.block
send_file_sync('spec/fixtures/image.png', 'image/png')
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)

wall.release

wait(0.5.seconds).for(command).to be_finished

expect('img').to be_loaded_image
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)
it 'waits until the has failed to load' do
App.start_html = <<~HTML
<a href="#" onclick="
let img = document.createElement('img');
img.src = '/next';
document.body.append(img);
">label</a>
HTML

wall.release
wall = Wall.new
App.next_action = -> do
wall.block
halt 404
end

wait(0.5.seconds).for(command).to be_finished
visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)

wall.release

wait(0.5.seconds).for(command).to be_finished

expect('img').to be_broken_image
end

it 'does not wait forever for an image with a data: source' do
App.start_html = <<~HTML
<a href="#" onclick="
let img = document.createElement('img');
img.src = `data:image/png;base64,#{Base64.encode64(File.read('spec/fixtures/image.png')).gsub("\n", '')}`;
document.body.append(img);
">label</a>
HTML

visit '/start'
command = ObservableCommand.new { page.find('a').click }
command.execute

wait(0.1.seconds).for(command).to be_finished

expect('img').to be_loaded_image
end

it 'does not wait for an image with [loading=lazy]' do
App.start_html = <<~HTML
<a href="#" onclick="
let img = document.createElement('img');
img.setAttribute('loading', 'lazy');
img.src =' /next';
document.body.append(img);
">label</a>
#{(1...500).map { |i| "<p>#{i}</p>" }.join}
HTML

server_spy = double('server action', reached: nil)

App.next_action = -> do
server_spy.reached
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
command.execute

wait(0.1.seconds).for(command).to be_finished

expect(server_spy).to_not have_received(:reached)
end

expect('img').to be_loaded_image
end

it 'waits until a dynamically inserted image has failed to load' do
App.start_html = <<~HTML
<a href="#" onclick="img = document.createElement('img'); img.src = '/next'; document.body.append(img)">label</a>
HTML
describe 'dynamically inserted iframes' do

wall = Wall.new
App.next_action = -> do
wall.block
halt 404
it 'waits until the iframe has loaded' do
App.start_html = <<~HTML
<a href="#" onclick="
let iframe = document.createElement('iframe');
iframe.src = '/next';
document.body.append(iframe);
">label</a>
HTML

wall = Wall.new
App.next_action = -> do
wall.block
render_body('hello from iframe')
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)

wall.release

wait(0.5.seconds).for(command).to be_finished
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)
it 'waits until the iframe has failed to load' do
App.start_html = <<~HTML
<a href="#" onclick="
let iframe = document.createElement('iframe');
iframe.src = '/next';
document.body.append(iframe);
">label</a>
HTML

wall.release
wall = Wall.new
App.next_action = -> do
wall.block
halt 500
end

wait(0.5.seconds).for(command).to be_finished
visit '/start'
command = ObservableCommand.new { page.find('a').click }
expect(command).to run_into_wall(wall)

wall.release

wait(0.5.seconds).for(command).to be_finished
end

it 'does not wait forever for an iframe with a data: source' do
App.start_html = <<~HTML
<a href="#" onclick="
let iframe = document.createElement('iframe');
iframe.src = `data:text/html;base64,#{Base64.encode64('hello from iframe').gsub("\n", '')}`;
document.body.append(iframe);
">label</a>
HTML

visit '/start'
command = ObservableCommand.new { page.find('a').click }
command.execute

wait(0.1.seconds).for(command).to be_finished
end

it 'does not wait for an iframe with [loading=lazy]' do
App.start_html = <<~HTML
<a href="#" onclick="
let iframe = document.createElement('iframe');
iframe.setAttribute('loading', 'lazy');
iframe.src =' /next';
document.body.append(iframe);
">label</a>
#{(1...500).map { |i| "<p>#{i}</p>" }.join}
HTML

server_spy = double('server action', reached: nil)

App.next_action = -> do
server_spy.reached
end

visit '/start'
command = ObservableCommand.new { page.find('a').click }
command.execute

wait(0.1.seconds).for(command).to be_finished

expect(server_spy).to_not have_received(:reached)
end

expect('img').to be_broken_image
end

describe 'dynamically inserted video elements' do
Expand Down

0 comments on commit 963300d

Please sign in to comment.