3330 lines
116 KiB
JavaScript
3330 lines
116 KiB
JavaScript
|
/**
|
||
|
* Copyright 2016 Google Inc. All Rights Reserved.
|
||
|
*
|
||
|
* Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
|
||
|
*
|
||
|
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
|
||
|
// Sets the timeout to three times the poll interval to ensure all updates
|
||
|
// happen (especially in slower browsers). Native implementations get the
|
||
|
// standard 100ms timeout defined in the spec.
|
||
|
var ASYNC_TIMEOUT = IntersectionObserver.prototype.THROTTLE_TIMEOUT * 3 || 100;
|
||
|
|
||
|
|
||
|
var io;
|
||
|
var noop = function() {};
|
||
|
|
||
|
|
||
|
// References to DOM elements, which are accessible to any test
|
||
|
// and reset prior to each test so state isn't shared.
|
||
|
var rootEl;
|
||
|
var grandParentEl;
|
||
|
var parentEl;
|
||
|
var targetEl1;
|
||
|
var targetEl2;
|
||
|
var targetEl3;
|
||
|
var targetEl4;
|
||
|
|
||
|
|
||
|
describe('IntersectionObserver', function() {
|
||
|
|
||
|
before(function() {
|
||
|
// If the browser running the tests doesn't support MutationObserver,
|
||
|
// fall back to polling.
|
||
|
if (!('MutationObserver' in window)) {
|
||
|
IntersectionObserver.prototype.POLL_INTERVAL =
|
||
|
IntersectionObserver.prototype.THROTTLE_TIMEOUT || 100;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
beforeEach(function() {
|
||
|
addStyles();
|
||
|
addFixtures();
|
||
|
});
|
||
|
|
||
|
|
||
|
afterEach(function() {
|
||
|
if (io && 'disconnect' in io) io.disconnect();
|
||
|
io = null;
|
||
|
|
||
|
removeStyles();
|
||
|
removeFixtures();
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('constructor', function() {
|
||
|
|
||
|
it('throws when callback is not a function', function() {
|
||
|
expect(function() {
|
||
|
io = new IntersectionObserver(null);
|
||
|
}).to.throwException();
|
||
|
});
|
||
|
|
||
|
|
||
|
it('instantiates root correctly', function() {
|
||
|
io = new IntersectionObserver(noop);
|
||
|
expect(io.root).to.be(null);
|
||
|
|
||
|
io = new IntersectionObserver(noop, {root: document});
|
||
|
expect(io.root).to.be(document);
|
||
|
|
||
|
io = new IntersectionObserver(noop, {root: rootEl});
|
||
|
expect(io.root).to.be(rootEl);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('throws when root is not a Document or Element', function() {
|
||
|
expect(function() {
|
||
|
io = new IntersectionObserver(noop, {root: 'foo'});
|
||
|
}).to.throwException();
|
||
|
});
|
||
|
|
||
|
|
||
|
it('instantiates rootMargin correctly', function() {
|
||
|
io = new IntersectionObserver(noop, {rootMargin: '10px'});
|
||
|
expect(io.rootMargin).to.be('10px 10px 10px 10px');
|
||
|
|
||
|
io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
|
||
|
expect(io.rootMargin).to.be('10px -5% 10px -5%');
|
||
|
|
||
|
io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
|
||
|
expect(io.rootMargin).to.be('10px 20% 0px 20%');
|
||
|
|
||
|
io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
|
||
|
expect(io.rootMargin).to.be('0px 0px -5% 5px');
|
||
|
|
||
|
// TODO(philipwalton): the polyfill supports fractional pixel and
|
||
|
// percentage values, but the native Chrome implementation does not,
|
||
|
// at least not in what it reports `rootMargin` to be.
|
||
|
if (!supportsNativeIntersectionObserver()) {
|
||
|
io = new IntersectionObserver(noop, {rootMargin: '-2.5% -8.5px'});
|
||
|
expect(io.rootMargin).to.be('-2.5% -8.5px -2.5% -8.5px');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
// TODO(philipwalton): this doesn't throw in FF, consider readding once
|
||
|
// expected behavior is clarified.
|
||
|
// it('throws when rootMargin is not in pixels or pecernt', function() {
|
||
|
// expect(function() {
|
||
|
// io = new IntersectionObserver(noop, {rootMargin: '0'});
|
||
|
// }).to.throwException();
|
||
|
// });
|
||
|
|
||
|
|
||
|
// Chrome's implementation in version 51 doesn't include the thresholds
|
||
|
// property, but versions 52+ do.
|
||
|
if ('thresholds' in IntersectionObserver.prototype) {
|
||
|
it('instantiates thresholds correctly', function() {
|
||
|
io = new IntersectionObserver(noop);
|
||
|
expect(io.thresholds).to.eql([0]);
|
||
|
|
||
|
io = new IntersectionObserver(noop, {threshold: 0.5});
|
||
|
expect(io.thresholds).to.eql([0.5]);
|
||
|
|
||
|
io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
|
||
|
expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
|
||
|
|
||
|
io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
|
||
|
expect(io.thresholds).to.eql([0, .5, 1]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
it('throws when a threshold is not a number', function() {
|
||
|
expect(function() {
|
||
|
io = new IntersectionObserver(noop, {threshold: ['foo']});
|
||
|
}).to.throwException();
|
||
|
});
|
||
|
|
||
|
|
||
|
it('throws when a threshold value is not between 0 and 1', function() {
|
||
|
expect(function() {
|
||
|
io = new IntersectionObserver(noop, {threshold: [0, -1]});
|
||
|
}).to.throwException();
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('observe', function() {
|
||
|
|
||
|
it('throws when target is not an Element', function() {
|
||
|
expect(function() {
|
||
|
io = new IntersectionObserver(noop);
|
||
|
io.observe(null);
|
||
|
}).to.throwException();
|
||
|
});
|
||
|
|
||
|
it('fills in x and y in the resulting rects', function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
var entry = records[0];
|
||
|
expect(entry.rootBounds.x).to.be(entry.rootBounds.left);
|
||
|
expect(entry.rootBounds.y).to.be(entry.rootBounds.top);
|
||
|
expect(entry.boundingClientRect.x).to.be(entry.boundingClientRect.left);
|
||
|
expect(entry.boundingClientRect.y).to.be(entry.boundingClientRect.top);
|
||
|
expect(entry.intersectionRect.x).to.be(entry.intersectionRect.left);
|
||
|
expect(entry.intersectionRect.y).to.be(entry.intersectionRect.top);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
targetEl2.style.top = '-40px';
|
||
|
io.observe(targetEl1);
|
||
|
});
|
||
|
|
||
|
it('triggers for all targets when observing begins', function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
|
||
|
targetEl2.style.top = '-40px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
});
|
||
|
|
||
|
it('triggers for existing targets when observing begins after monitoring has begun', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
setTimeout(function() {
|
||
|
io.observe(targetEl2);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('triggers with the correct arguments', function(done) {
|
||
|
io = new IntersectionObserver(function(records, observer) {
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
|
||
|
expect(records[1] instanceof IntersectionObserverEntry).to.be.ok();
|
||
|
expect(observer).to.be(io);
|
||
|
expect(this).to.be(io);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
|
||
|
targetEl2.style.top = '-40px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles container elements with non-visible overflow',
|
||
|
function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
io.observe(targetEl1);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.left = '-40px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
parentEl.style.overflow = 'visible';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('observes one target at a single threshold correctly', function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
targetEl1.style.left = '-5px';
|
||
|
io.observe(targetEl1);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.left = '-15px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be.lessThan(0.5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.left = '-25px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.left = '-10px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
it('observes multiple targets at multiple thresholds correctly',
|
||
|
function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {
|
||
|
root: rootEl,
|
||
|
threshold: [1, 0.5, 0]
|
||
|
});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '-15px';
|
||
|
targetEl2.style.top = '-5px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
targetEl3.style.top = '0px';
|
||
|
targetEl3.style.left = '205px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
io.observe(targetEl3);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(3);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.25);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0.75);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '-5px';
|
||
|
targetEl2.style.top = '-15px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
targetEl3.style.top = '0px';
|
||
|
targetEl3.style.left = '195px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(3);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.75);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0.25);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0.25);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '5px';
|
||
|
targetEl2.style.top = '-25px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
targetEl3.style.top = '0px';
|
||
|
targetEl3.style.left = '185px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(3);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0.75);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '15px';
|
||
|
targetEl2.style.top = '-35px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
targetEl3.style.top = '0px';
|
||
|
targetEl3.style.left = '175px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(4);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].target).to.be(targetEl3);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles rootMargin properly', function(done) {
|
||
|
|
||
|
parentEl.style.overflow = 'visible';
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '-20px';
|
||
|
targetEl2.style.top = '-20px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
targetEl3.style.top = '0px';
|
||
|
targetEl3.style.left = '200px';
|
||
|
targetEl4.style.top = '180px';
|
||
|
targetEl4.style.left = '180px';
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
records = sortRecords(records);
|
||
|
expect(records.length).to.be(4);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(.5);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(.5);
|
||
|
expect(records[3].target).to.be(targetEl4);
|
||
|
expect(records[3].intersectionRatio).to.be(1);
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}, {root: rootEl, rootMargin: '10px'});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
io.observe(targetEl3);
|
||
|
io.observe(targetEl4);
|
||
|
},
|
||
|
function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
records = sortRecords(records);
|
||
|
expect(records.length).to.be(4);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.5);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0.5);
|
||
|
expect(records[3].target).to.be(targetEl4);
|
||
|
expect(records[3].intersectionRatio).to.be(0.5);
|
||
|
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}, {root: rootEl, rootMargin: '-10px 10%'});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
io.observe(targetEl3);
|
||
|
io.observe(targetEl4);
|
||
|
},
|
||
|
function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
records = sortRecords(records);
|
||
|
expect(records.length).to.be(4);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.5);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0);
|
||
|
expect(records[3].target).to.be(targetEl4);
|
||
|
expect(records[3].intersectionRatio).to.be(0.5);
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}, {root: rootEl, rootMargin: '-5% -2.5% 0px'});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
io.observe(targetEl3);
|
||
|
io.observe(targetEl4);
|
||
|
},
|
||
|
function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
records = sortRecords(records);
|
||
|
expect(records.length).to.be(4);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(0.5);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(0.5);
|
||
|
expect(records[2].target).to.be(targetEl3);
|
||
|
expect(records[2].intersectionRatio).to.be(0);
|
||
|
expect(records[3].target).to.be(targetEl4);
|
||
|
expect(records[3].intersectionRatio).to.be(0.25);
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
io.observe(targetEl3);
|
||
|
io.observe(targetEl4);
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles targets on the boundary of root', function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '-21px';
|
||
|
targetEl2.style.top = '-20px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '-20px';
|
||
|
targetEl2.style.top = '-21px';
|
||
|
targetEl2.style.left = '0px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.top = '-20px';
|
||
|
targetEl1.style.left = '200px';
|
||
|
targetEl2.style.top = '200px';
|
||
|
targetEl2.style.left = '200px';
|
||
|
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles zero-size targets within the root coordinate space',
|
||
|
function(done) {
|
||
|
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
|
||
|
targetEl1.style.top = '0px';
|
||
|
targetEl1.style.left = '0px';
|
||
|
targetEl1.style.width = '0px';
|
||
|
targetEl1.style.height = '0px';
|
||
|
io.observe(targetEl1);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles elements with display set to none', function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
rootEl.style.display = 'none';
|
||
|
io.observe(targetEl1);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
rootEl.style.display = 'block';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
parentEl.style.display = 'none';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
parentEl.style.display = 'block';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(4);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
targetEl1.style.display = 'none';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(5);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
|
||
|
it('handles target elements not yet added to the DOM', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
// targetEl5 is initially not in the DOM. Note that this element must be
|
||
|
// created outside of the addFixtures() function to catch the IE11 error
|
||
|
// described here: https://github.com/w3c/IntersectionObserver/pull/205
|
||
|
var targetEl5 = document.createElement('div');
|
||
|
targetEl5.setAttribute('id', 'target5');
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
io.observe(targetEl5);
|
||
|
setTimeout(function() {
|
||
|
// Initial observe should trigger with no intersections since
|
||
|
// targetEl5 is not yet in the DOM.
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Adding targetEl5 inside rootEl should trigger.
|
||
|
parentEl.insertBefore(targetEl5, targetEl2);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].target).to.be(targetEl5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Removing an ancestor of targetEl5 should trigger.
|
||
|
grandParentEl.parentNode.removeChild(grandParentEl);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Adding the previously removed targetEl5 (via grandParentEl)
|
||
|
// back directly inside rootEl should trigger.
|
||
|
rootEl.appendChild(targetEl5);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(4);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].target).to.be(targetEl5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Removing rootEl should trigger.
|
||
|
rootEl.parentNode.removeChild(rootEl);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(5);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].target).to.be(targetEl5);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
|
||
|
if ('attachShadow' in Element.prototype) {
|
||
|
it('handles targets in shadow DOM', function(done) {
|
||
|
grandParentEl.attachShadow({mode: 'open'});
|
||
|
grandParentEl.shadowRoot.appendChild(parentEl);
|
||
|
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
});
|
||
|
|
||
|
it('handles roots in shadow DOM', function(done) {
|
||
|
var shadowRoot = grandParentEl.attachShadow({mode: 'open'});
|
||
|
|
||
|
shadowRoot.innerHTML =
|
||
|
'<style>' +
|
||
|
'#slot-parent {' +
|
||
|
' position: relative;' +
|
||
|
' width: 400px;' +
|
||
|
' height: 200px;' +
|
||
|
'}' +
|
||
|
'</style>' +
|
||
|
'<div id="slot-parent"><slot></slot></div>';
|
||
|
|
||
|
var slotParent = shadowRoot.getElementById('slot-parent');
|
||
|
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, {root: slotParent});
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
it('handles sub-root element scrolling', function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, {root: rootEl});
|
||
|
|
||
|
io.observe(targetEl3);
|
||
|
setTimeout(function() {
|
||
|
parentEl.scrollLeft = 40;
|
||
|
}, 0);
|
||
|
});
|
||
|
|
||
|
|
||
|
// Only run this test in browsers that support CSS transitions.
|
||
|
if ('transform' in document.documentElement.style &&
|
||
|
'transition' in document.documentElement.style) {
|
||
|
|
||
|
it('supports CSS transitions and transforms', function(done) {
|
||
|
|
||
|
targetEl1.style.top = '220px';
|
||
|
targetEl1.style.left = '220px';
|
||
|
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
expect(records.length).to.be(1);
|
||
|
// Chrome's native implementation sometimes incorrectly reports
|
||
|
// the intersection ratio as a number > 1.
|
||
|
expect(records[0].intersectionRatio >= 1);
|
||
|
done();
|
||
|
}, {root: rootEl, threshold: [1]});
|
||
|
|
||
|
// CSS transitions that are slower than the default throttle timeout
|
||
|
// require polling to detect, which can be set on a per-instance basis.
|
||
|
if (!supportsNativeIntersectionObserver()) {
|
||
|
io.POLL_INTERVAL = 100;
|
||
|
}
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
setTimeout(function() {
|
||
|
targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
|
||
|
}, 0);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
it('uses the viewport when no root is specified', function(done) {
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
var viewportWidth =
|
||
|
document.documentElement.clientWidth || document.body.clientWidth;
|
||
|
var viewportHeight =
|
||
|
document.documentElement.clientHeight || document.body.clientHeight;
|
||
|
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].rootBounds.top).to.be(0);
|
||
|
expect(records[0].rootBounds.left).to.be(0);
|
||
|
expect(records[0].rootBounds.right).to.be(viewportWidth);
|
||
|
expect(records[0].rootBounds.width).to.be(viewportWidth);
|
||
|
expect(records[0].rootBounds.bottom).to.be(viewportHeight);
|
||
|
expect(records[0].rootBounds.height).to.be(viewportHeight);
|
||
|
done();
|
||
|
});
|
||
|
|
||
|
// Ensures targetEl1 is visible in the viewport before observing.
|
||
|
window.scrollTo(0, 0);
|
||
|
rootEl.style.position = 'absolute';
|
||
|
rootEl.style.top = '0px';
|
||
|
rootEl.style.left = '0px';
|
||
|
|
||
|
io.observe(targetEl1);
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('takeRecords', function() {
|
||
|
|
||
|
it('supports getting records before the callback is invoked',
|
||
|
function(done) {
|
||
|
|
||
|
var lastestRecords = [];
|
||
|
io = new IntersectionObserver(function(records) {
|
||
|
lastestRecords = lastestRecords.concat(records);
|
||
|
}, {root: rootEl});
|
||
|
io.observe(targetEl1);
|
||
|
|
||
|
window.requestAnimationFrame && requestAnimationFrame(function() {
|
||
|
lastestRecords = lastestRecords.concat(io.takeRecords());
|
||
|
});
|
||
|
|
||
|
setTimeout(function() {
|
||
|
expect(lastestRecords.length).to.be(1);
|
||
|
expect(lastestRecords[0].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('unobserve', function() {
|
||
|
|
||
|
it('removes targets from the internal store', function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
targetEl1.style.top = targetEl2.style.top = '0px';
|
||
|
targetEl1.style.left = targetEl2.style.left = '0px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.unobserve(targetEl1);
|
||
|
targetEl1.style.top = targetEl2.style.top = '0px';
|
||
|
targetEl1.style.left = targetEl2.style.left = '-40px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].target).to.be(targetEl2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.unobserve(targetEl2);
|
||
|
targetEl1.style.top = targetEl2.style.top = '0px';
|
||
|
targetEl1.style.left = targetEl2.style.left = '0px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
describe('disconnect', function() {
|
||
|
|
||
|
it('removes all targets and stops listening for changes', function(done) {
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
io = new IntersectionObserver(spy, {root: rootEl});
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
targetEl1.style.top = targetEl2.style.top = '0px';
|
||
|
targetEl1.style.left = targetEl2.style.left = '0px';
|
||
|
io.observe(targetEl1);
|
||
|
io.observe(targetEl2);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].target).to.be(targetEl1);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].target).to.be(targetEl2);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
targetEl1.style.top = targetEl2.style.top = '0px';
|
||
|
targetEl1.style.left = targetEl2.style.left = '-40px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
}
|
||
|
], done);
|
||
|
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
describe('iframe', function() {
|
||
|
var iframe;
|
||
|
var iframeWin, iframeDoc;
|
||
|
var documentElement, body;
|
||
|
var iframeTargetEl1, iframeTargetEl2;
|
||
|
var bodyWidth;
|
||
|
|
||
|
beforeEach(function(done) {
|
||
|
iframe = document.createElement('iframe');
|
||
|
iframe.setAttribute('frameborder', '0');
|
||
|
iframe.setAttribute('scrolling', 'yes');
|
||
|
iframe.style.position = 'fixed';
|
||
|
iframe.style.top = '0px';
|
||
|
iframe.style.width = '100px';
|
||
|
iframe.style.height = '200px';
|
||
|
iframe.onerror = function() {
|
||
|
done(new Error('iframe initialization failed'));
|
||
|
};
|
||
|
iframe.onload = function() {
|
||
|
iframe.onload = null;
|
||
|
iframeWin = iframe.contentWindow;
|
||
|
iframeDoc = iframeWin.document;
|
||
|
iframeDoc.open();
|
||
|
iframeDoc.write('<!DOCTYPE html><html><body>');
|
||
|
iframeDoc.write('<style>');
|
||
|
iframeDoc.write('body {margin: 0}');
|
||
|
iframeDoc.write('.target {height: 200px; margin-bottom: 2px; background: blue;}');
|
||
|
iframeDoc.write('</style>');
|
||
|
iframeDoc.close();
|
||
|
|
||
|
// Ensure the documentElement and body are always sorted on top. See
|
||
|
// `sortRecords` for more info.
|
||
|
documentElement = iframeDoc.documentElement;
|
||
|
body = iframeDoc.body;
|
||
|
documentElement.id = 'A1';
|
||
|
body.id = 'A1';
|
||
|
|
||
|
function createTarget(id, bg) {
|
||
|
var target = iframeDoc.createElement('div');
|
||
|
target.id = id;
|
||
|
target.className = 'target';
|
||
|
target.style.background = bg;
|
||
|
iframeDoc.body.appendChild(target);
|
||
|
return target;
|
||
|
}
|
||
|
iframeTargetEl1 = createTarget('target1', 'blue');
|
||
|
iframeTargetEl2 = createTarget('target2', 'green');
|
||
|
bodyWidth = iframeDoc.body.clientWidth;
|
||
|
done();
|
||
|
};
|
||
|
iframe.src = 'about:blank';
|
||
|
rootEl.appendChild(iframe);
|
||
|
});
|
||
|
|
||
|
afterEach(function() {
|
||
|
rootEl.removeChild(iframe);
|
||
|
});
|
||
|
|
||
|
function rect(r) {
|
||
|
return {
|
||
|
y: typeof r.y == 'number' ? r.y : r.top,
|
||
|
x: typeof r.x == 'number' ? r.x : r.left,
|
||
|
top: r.top,
|
||
|
left: r.left,
|
||
|
width: r.width != null ? r.width : r.right - r.left,
|
||
|
height: r.height != null ? r.height : r.bottom - r.top,
|
||
|
right: r.right != null ? r.right : r.left + r.width,
|
||
|
bottom: r.bottom != null ? r.bottom : r.top + r.height
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getRootRect(doc) {
|
||
|
var html = doc.documentElement;
|
||
|
var body = doc.body;
|
||
|
return rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
right: html.clientWidth || body.clientWidth,
|
||
|
width: html.clientWidth || body.clientWidth,
|
||
|
bottom: html.clientHeight || body.clientHeight,
|
||
|
height: html.clientHeight || body.clientHeight
|
||
|
});
|
||
|
}
|
||
|
|
||
|
describe('same-origin iframe loaded in the mainframe', function() {
|
||
|
it('iframe targets do not intersect with a top root element', function(done) {
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {root: rootEl});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('triggers for all targets in top-level root', function(done) {
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
|
||
|
// The rootBounds is for the document's root.
|
||
|
expect(records[0].rootBounds.height).to.be(innerHeight);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('triggers for all targets in iframe-level root', function(done) {
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
|
||
|
// The rootBounds is for the document's root.
|
||
|
expect(rect(records[0].rootBounds)).
|
||
|
to.eql(rect(iframeDoc.body.getBoundingClientRect()));
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {root: iframeDoc.body});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible frame', function(done) {
|
||
|
iframe.style.top = '0px';
|
||
|
iframe.style.height = '300px';
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible and offset frame', function(done) {
|
||
|
iframe.style.top = '10px';
|
||
|
iframe.style.height = '300px';
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top', function(done) {
|
||
|
iframe.style.top = '-10px';
|
||
|
iframe.style.height = '300px';
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.49
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on bottom', function(done) {
|
||
|
iframe.style.top = 'auto';
|
||
|
iframe.style.bottom = '-10px';
|
||
|
iframe.style.height = '300px';
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300 - 10
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.43, 0.45); // ~0.44
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible frame and scrolled', function(done) {
|
||
|
iframe.style.top = '0px';
|
||
|
iframe.style.height = '300px';
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Height is only for the visible area.
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top and scrolled', function(done) {
|
||
|
iframe.style.top = '-10px';
|
||
|
iframe.style.height = '300px';
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var io = new IntersectionObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
|
||
|
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
// The height is less by both: offset and scroll.
|
||
|
height: 200 - 10 - 10
|
||
|
});
|
||
|
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[0].intersectionRatio).to.within(0.89, 0.91); // ~0.9
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('handles tracking iframe viewport', function(done) {
|
||
|
iframe.style.height = '100px';
|
||
|
iframe.style.top = '100px';
|
||
|
iframeWin.scrollTo(0, 110);
|
||
|
// {root:iframeDoc} means to track the iframe viewport.
|
||
|
var io = new IntersectionObserver(
|
||
|
function (records) {
|
||
|
io.unobserve(iframeTargetEl1);
|
||
|
|
||
|
var intersectionRect = rect({
|
||
|
top: 0, // if root=null, then this would be 100.
|
||
|
left: 0,
|
||
|
height: 90,
|
||
|
width: bodyWidth
|
||
|
});
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
|
||
|
done();
|
||
|
},
|
||
|
{ root: iframeDoc }
|
||
|
);
|
||
|
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('handles tracking iframe viewport with rootMargin', function(done) {
|
||
|
iframe.style.height = '100px';
|
||
|
|
||
|
var io = new IntersectionObserver(
|
||
|
function (records) {
|
||
|
io.unobserve(iframeTargetEl1);
|
||
|
var intersectionRect = rect({
|
||
|
top: 0, // if root=null, then this would be 100.
|
||
|
left: 0,
|
||
|
height: 200,
|
||
|
width: bodyWidth
|
||
|
});
|
||
|
|
||
|
// rootMargin: 100% --> 3x width + 3x height.
|
||
|
var expectedRootBounds = rect({
|
||
|
top: -100,
|
||
|
left: -bodyWidth,
|
||
|
width: bodyWidth * 3,
|
||
|
height: 100 * 3
|
||
|
});
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
|
||
|
done();
|
||
|
},
|
||
|
{ root: iframeDoc, rootMargin: '100%' }
|
||
|
);
|
||
|
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
// Current spec indicates that cross-document tracking yields
|
||
|
// an essentially empty IntersectionObserverEntry.
|
||
|
// See: https://github.com/w3c/IntersectionObserver/issues/87
|
||
|
it('does not track cross-document elements', function(done) {
|
||
|
var io = new IntersectionObserver(
|
||
|
function (records) {
|
||
|
io.unobserve(iframeTargetEl1)
|
||
|
|
||
|
expect(records.length).to.be(1);
|
||
|
const zeroesRect = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
});
|
||
|
expect(rect(records[0].rootBounds)).to.eql(zeroesRect);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(zeroesRect);
|
||
|
expect(records.isIntersecting).false;
|
||
|
done();
|
||
|
},
|
||
|
{ root: document }
|
||
|
);
|
||
|
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('handles style changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
// When first element becomes invisible, the second element will show.
|
||
|
// And in reverse: when the first element becomes visible again, the
|
||
|
// second element will disappear.
|
||
|
var io = new IntersectionObserver(spy);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = 'none';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = '';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles scroll changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
// Scrolling to the middle of the iframe shows the second box and
|
||
|
// hides the first.
|
||
|
var io = new IntersectionObserver(spy);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 202);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 0);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles iframe changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
// Iframe goes off screen and returns.
|
||
|
var io = new IntersectionObserver(spy);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].rootBounds.height).to.be(innerHeight);
|
||
|
expect(records[0].intersectionRect.height).to.be(200);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Completely off screen.
|
||
|
iframe.style.top = '-202px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].rootBounds.height).to.be(innerHeight);
|
||
|
expect(records[0].intersectionRect.height).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Partially returns.
|
||
|
iframe.style.top = '-100px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].rootBounds.height).to.be(innerHeight);
|
||
|
expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('continues to monitor until the last target unobserved', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
// Iframe goes off screen and returns.
|
||
|
var io = new IntersectionObserver(spy);
|
||
|
io.observe(target1);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
expect(spy.lastCall.args[0].length).to.be(3);
|
||
|
|
||
|
// Unobserve one from the main context and one from iframe.
|
||
|
io.unobserve(target1);
|
||
|
io.unobserve(iframeTargetEl2);
|
||
|
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Completely off screen.
|
||
|
iframe.style.top = '-202px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
expect(spy.lastCall.args[0].length).to.be(1);
|
||
|
|
||
|
io.unobserve(iframeTargetEl1);
|
||
|
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Partially returns.
|
||
|
iframe.style.top = '-100px';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('same-origin iframe loaded in an iframe', function() {
|
||
|
var ASYNC_TIMEOUT = 300;
|
||
|
|
||
|
beforeEach(function(done) {
|
||
|
/* Uncomment these lines to force polyfill inside the iframe.
|
||
|
delete iframeWin.IntersectionObserver;
|
||
|
delete iframeWin.IntersectionObserverEntry;
|
||
|
*/
|
||
|
|
||
|
// Install polyfill right into the iframe.
|
||
|
if (!iframeWin.IntersectionObserver) {
|
||
|
var script = iframeDoc.createElement('script');
|
||
|
script.src = 'intersection-observer.js';
|
||
|
script.onload = function() {
|
||
|
done();
|
||
|
};
|
||
|
iframeDoc.body.appendChild(script);
|
||
|
} else {
|
||
|
done();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function computeRectIntersection(rect1, rect2) {
|
||
|
var top = Math.max(rect1.top, rect2.top);
|
||
|
var bottom = Math.min(rect1.bottom, rect2.bottom);
|
||
|
var left = Math.max(rect1.left, rect2.left);
|
||
|
var right = Math.min(rect1.right, rect2.right);
|
||
|
var width = right - left;
|
||
|
var height = bottom - top;
|
||
|
|
||
|
return (width >= 0 && height >= 0) && {
|
||
|
top: top,
|
||
|
bottom: bottom,
|
||
|
left: left,
|
||
|
right: right,
|
||
|
width: width,
|
||
|
height: height
|
||
|
} || {
|
||
|
top: 0,
|
||
|
bottom: 0,
|
||
|
left: 0,
|
||
|
right: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function checkRootBounds(records) {
|
||
|
if (!supportsNativeIntersectionObserver(iframeWin)) {
|
||
|
records.forEach(function(record) {
|
||
|
expect(rect(record.rootBounds)).to.eql(getRootRect(document));
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function applyParentRect(parentRect) {
|
||
|
iframe.style.top = parentRect.top + 'px';
|
||
|
iframe.style.left = parentRect.left + 'px';
|
||
|
iframe.style.height = parentRect.height + 'px';
|
||
|
iframe.style.width = parentRect.width + 'px';
|
||
|
}
|
||
|
|
||
|
function createObserver(callback, options, parentRect) {
|
||
|
var io = new iframeWin.IntersectionObserver(callback, options);
|
||
|
if (parentRect) {
|
||
|
applyParentRect(parentRect);
|
||
|
}
|
||
|
return io;
|
||
|
}
|
||
|
|
||
|
it('calculates rects for a fully visible frame', function(done) {
|
||
|
var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible and offset frame', function(done) {
|
||
|
var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top', function(done) {
|
||
|
var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on bottom', function(done) {
|
||
|
var rootRect = getRootRect(document);
|
||
|
var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible and scrolled frame', function(done) {
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Height is only for the visible area.
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top and scrolled', function(done) {
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(4);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
// The height is less by both: offset and scroll.
|
||
|
height: 200 - 10 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[3].isIntersecting).to.be(true);
|
||
|
expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully clipped frame', function(done) {
|
||
|
var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBounds(records);
|
||
|
|
||
|
var emptyRect = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
});
|
||
|
|
||
|
// The documentElement is completely invisible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
|
||
|
// The document.body is completely invisible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
|
||
|
// The target1 is completely invisible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[2].isIntersecting).to.be(false);
|
||
|
expect(records[2].intersectionRatio).to.be(0);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('handles style changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// When first element becomes invisible, the second element will show.
|
||
|
// And in reverse: when the first element becomes visible again, the
|
||
|
// second element will disappear.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = 'none';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = '';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles scroll changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// Scrolling to the middle of the iframe shows the second box and
|
||
|
// hides the first.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 202);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 0);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles parent rect changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// Iframe goes off screen and returns.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
checkRootBounds(records);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height).to.be(200);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Completely off screen.
|
||
|
applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
checkRootBounds(records);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Partially returns.
|
||
|
applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
checkRootBounds(records);
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('cross-origin iframe', function() {
|
||
|
var ASYNC_TIMEOUT = 300;
|
||
|
var crossOriginUpdater;
|
||
|
|
||
|
beforeEach(function(done) {
|
||
|
Object.defineProperty(iframeWin, 'frameElement', {value: null});
|
||
|
|
||
|
/* Uncomment these lines to force polyfill inside the iframe.
|
||
|
delete iframeWin.IntersectionObserver;
|
||
|
delete iframeWin.IntersectionObserverEntry;
|
||
|
*/
|
||
|
|
||
|
// Install polyfill right into the iframe.
|
||
|
if (!iframeWin.IntersectionObserver) {
|
||
|
var script = iframeDoc.createElement('script');
|
||
|
script.src = 'intersection-observer.js';
|
||
|
script.onload = function() {
|
||
|
if (iframeWin.IntersectionObserver._setupCrossOriginUpdater) {
|
||
|
crossOriginUpdater = iframeWin.IntersectionObserver._setupCrossOriginUpdater();
|
||
|
}
|
||
|
done();
|
||
|
};
|
||
|
iframeDoc.body.appendChild(script);
|
||
|
} else {
|
||
|
done();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
afterEach(function() {
|
||
|
if (IntersectionObserver._resetCrossOriginUpdater) {
|
||
|
IntersectionObserver._resetCrossOriginUpdater();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function computeRectIntersection(rect1, rect2) {
|
||
|
var top = Math.max(rect1.top, rect2.top);
|
||
|
var bottom = Math.min(rect1.bottom, rect2.bottom);
|
||
|
var left = Math.max(rect1.left, rect2.left);
|
||
|
var right = Math.min(rect1.right, rect2.right);
|
||
|
var width = right - left;
|
||
|
var height = bottom - top;
|
||
|
|
||
|
return (width >= 0 && height >= 0) && {
|
||
|
top: top,
|
||
|
bottom: bottom,
|
||
|
left: left,
|
||
|
right: right,
|
||
|
width: width,
|
||
|
height: height
|
||
|
} || {
|
||
|
top: 0,
|
||
|
bottom: 0,
|
||
|
left: 0,
|
||
|
right: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function checkRootBoundsAreNull(records) {
|
||
|
if (!supportsNativeIntersectionObserver(iframeWin)) {
|
||
|
records.forEach(function(record) {
|
||
|
expect(record.rootBounds).to.be(null);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function applyParentRect(parentRect) {
|
||
|
if (crossOriginUpdater) {
|
||
|
var parentIntersectionRect = computeRectIntersection(
|
||
|
parentRect, getRootRect(document));
|
||
|
crossOriginUpdater(parentRect, parentIntersectionRect);
|
||
|
} else {
|
||
|
iframe.style.top = parentRect.top + 'px';
|
||
|
iframe.style.left = parentRect.left + 'px';
|
||
|
iframe.style.height = parentRect.height + 'px';
|
||
|
iframe.style.width = parentRect.width + 'px';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createObserver(callback, options, parentRect) {
|
||
|
var io = new iframeWin.IntersectionObserver(callback, options);
|
||
|
if (parentRect) {
|
||
|
applyParentRect(parentRect);
|
||
|
}
|
||
|
return io;
|
||
|
}
|
||
|
|
||
|
it('calculates rects for a fully visible frame', function(done) {
|
||
|
var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible and offset frame', function(done) {
|
||
|
var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top', function(done) {
|
||
|
var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on bottom', function(done) {
|
||
|
var rootRect = getRootRect(document);
|
||
|
var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.be(1);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully visible and scrolled frame', function(done) {
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// 300 / 404 == ~0.743
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// 300 / 402 == ~0.746
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
|
||
|
|
||
|
// The target1 is fully visible.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Height is only for the visible area.
|
||
|
height: 200 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a clipped frame on top and scrolled', function(done) {
|
||
|
iframeWin.scrollTo(0, 10);
|
||
|
var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(4);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
// The documentElement is partially visible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 404 == ~0.717
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
|
||
|
|
||
|
// The document.body is partially visible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(rect({
|
||
|
top: 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 300 - 10
|
||
|
}));
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
// (300 - 10) / 402 == ~0.721
|
||
|
expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
|
||
|
|
||
|
// The target1 is clipped at the top by the iframe's clipping.
|
||
|
var clientRect1 = rect({
|
||
|
top: -10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect1 = rect({
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// Top is clipped.
|
||
|
top: 10,
|
||
|
// The height is less by both: offset and scroll.
|
||
|
height: 200 - 10 - 10
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
|
||
|
expect(records[2].isIntersecting).to.be(true);
|
||
|
expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
|
||
|
|
||
|
// The target2 is partially visible.
|
||
|
var clientRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
var intersectRect2 = rect({
|
||
|
top: 202 - 10,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
// The bottom is clipped off.
|
||
|
bottom: 300
|
||
|
});
|
||
|
expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
|
||
|
expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
|
||
|
expect(records[3].isIntersecting).to.be(true);
|
||
|
expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
});
|
||
|
|
||
|
it('calculates rects for a fully clipped frame', function(done) {
|
||
|
var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
|
||
|
var io = createObserver(function(unsortedRecords) {
|
||
|
var records = sortRecords(unsortedRecords);
|
||
|
expect(records.length).to.be(3);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
|
||
|
var emptyRect = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
});
|
||
|
|
||
|
// The documentElement is completely invisible.
|
||
|
expect(rect(records[0].boundingClientRect))
|
||
|
.to.eql(rect(documentElement.getBoundingClientRect()));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
|
||
|
// The document.body is completely invisible.
|
||
|
expect(rect(records[1].boundingClientRect))
|
||
|
.to.eql(rect(body.getBoundingClientRect()));
|
||
|
expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
|
||
|
// The target1 is completely invisible.
|
||
|
var clientRect1 = rect({
|
||
|
top: 0,
|
||
|
left: 0,
|
||
|
width: bodyWidth,
|
||
|
height: 200
|
||
|
});
|
||
|
expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
|
||
|
expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
|
||
|
expect(records[2].isIntersecting).to.be(false);
|
||
|
expect(records[2].intersectionRatio).to.be(0);
|
||
|
|
||
|
done();
|
||
|
io.disconnect();
|
||
|
}, {}, parentRect);
|
||
|
io.observe(documentElement);
|
||
|
io.observe(body);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('blocks until crossOriginUpdater is called first time', function(done) {
|
||
|
if (supportsNativeIntersectionObserver(iframeWin)) {
|
||
|
// Skip: not possible to emulate with the native observer.
|
||
|
done();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
|
||
|
|
||
|
var io = createObserver(spy, {});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(0);
|
||
|
|
||
|
// Issue the first update.
|
||
|
crossOriginUpdater(parentRect, null);
|
||
|
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('doesn\'t block with a root specified', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var io = createObserver(spy, {root: body});
|
||
|
io.observe(iframeTargetEl1);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var record = sortRecords(spy.lastCall.args[0])[0];
|
||
|
expect(record.intersectionRatio).to.be(1);
|
||
|
expect(record.isIntersecting).to.be(true);
|
||
|
expect(rect(record.rootBounds)).to.eql(rect(body.getBoundingClientRect()));
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles style changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// When first element becomes invisible, the second element will show.
|
||
|
// And in reverse: when the first element becomes visible again, the
|
||
|
// second element will disappear.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = 'none';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeTargetEl1.style.display = '';
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles scroll changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// Scrolling to the middle of the iframe shows the second box and
|
||
|
// hides the first.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 202);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
expect(records[1].intersectionRatio).to.be(1);
|
||
|
expect(records[1].isIntersecting).to.be(true);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
iframeWin.scrollTo(0, 0);
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles parent rect changes', function(done) {
|
||
|
var spy = sinon.spy();
|
||
|
|
||
|
var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
|
||
|
|
||
|
// Iframe goes off screen and returns.
|
||
|
var io = createObserver(spy, {}, parentRect);
|
||
|
io.observe(iframeTargetEl1);
|
||
|
io.observe(iframeTargetEl2);
|
||
|
|
||
|
runSequence([
|
||
|
function(done) {
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(1);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(2);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
expect(records[0].intersectionRatio).to.be(1);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
expect(records[1].intersectionRatio).to.be(0);
|
||
|
expect(records[1].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height).to.be(200);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Completely off screen.
|
||
|
applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(2);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
expect(records[0].intersectionRatio).to.be(0);
|
||
|
expect(records[0].isIntersecting).to.be(false);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height).to.be(0);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
// Partially returns.
|
||
|
applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
|
||
|
setTimeout(function() {
|
||
|
expect(spy.callCount).to.be(3);
|
||
|
var records = sortRecords(spy.lastCall.args[0]);
|
||
|
expect(records.length).to.be(1);
|
||
|
checkRootBoundsAreNull(records);
|
||
|
expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
|
||
|
expect(records[0].isIntersecting).to.be(true);
|
||
|
// Top-level bounds.
|
||
|
expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
|
||
|
done();
|
||
|
}, ASYNC_TIMEOUT);
|
||
|
},
|
||
|
function(done) {
|
||
|
io.disconnect();
|
||
|
done();
|
||
|
}
|
||
|
], done);
|
||
|
});
|
||
|
|
||
|
it('handles tracking iframe viewport', function(done) {
|
||
|
iframe.style.height = '100px';
|
||
|
iframe.style.top = '100px';
|
||
|
iframeWin.scrollTo(0, 110);
|
||
|
// {root:iframeDoc} means to track the iframe viewport.
|
||
|
var io = createObserver(
|
||
|
function (records) {
|
||
|
io.unobserve(iframeTargetEl1);
|
||
|
var intersectionRect = rect({
|
||
|
top: 0, // if root=null, then this would be 100.
|
||
|
left: 0,
|
||
|
height: 90,
|
||
|
width: bodyWidth
|
||
|
});
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
|
||
|
done();
|
||
|
},
|
||
|
{ root: iframeDoc }
|
||
|
);
|
||
|
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
|
||
|
it('handles tracking iframe viewport with rootMargin', function(done) {
|
||
|
iframe.style.height = '100px';
|
||
|
|
||
|
var io = createObserver(
|
||
|
function (records) {
|
||
|
io.unobserve(iframeTargetEl1);
|
||
|
var intersectionRect = rect({
|
||
|
top: 0, // if root=null, then this would be 100.
|
||
|
left: 0,
|
||
|
height: 200,
|
||
|
width: bodyWidth
|
||
|
});
|
||
|
|
||
|
// rootMargin: 100% --> 3x width + 3x height.
|
||
|
var expectedRootBounds = rect({
|
||
|
top: -100,
|
||
|
left: -bodyWidth,
|
||
|
width: bodyWidth * 3,
|
||
|
height: 100 * 3
|
||
|
});
|
||
|
expect(records.length).to.be(1);
|
||
|
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
|
||
|
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
|
||
|
done();
|
||
|
},
|
||
|
{ root: iframeDoc, rootMargin: '100%' }
|
||
|
);
|
||
|
|
||
|
io.observe(iframeTargetEl1);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Runs a sequence of function and when finished invokes the done callback.
|
||
|
* Each function in the sequence is invoked with its own done function and
|
||
|
* it should call that function once it's complete.
|
||
|
* @param {Array<Function>} functions An array of async functions.
|
||
|
* @param {Function} done A final callback to be invoked once all function
|
||
|
* have run.
|
||
|
*/
|
||
|
function runSequence(functions, done) {
|
||
|
var next = functions.shift();
|
||
|
if (next) {
|
||
|
next(function() {
|
||
|
runSequence(functions, done);
|
||
|
});
|
||
|
} else {
|
||
|
done && done();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns whether or not the current browser has native support for
|
||
|
* IntersectionObserver.
|
||
|
* @param {Window=} win
|
||
|
* @return {boolean} True if native support is detected.
|
||
|
*/
|
||
|
function supportsNativeIntersectionObserver(win) {
|
||
|
win = win || window;
|
||
|
return 'IntersectionObserver' in win &&
|
||
|
win.IntersectionObserver.toString().indexOf('[native code]') > -1;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Sorts an array of records alphebetically by ascending ID. Since the current
|
||
|
* native implementation doesn't sort change entries by `observe` order, we do
|
||
|
* that ourselves for the non-polyfill case. Since all tests call observe
|
||
|
* on targets in sequential order, this should always match.
|
||
|
* https://crbug.com/613679
|
||
|
* @param {Array<IntersectionObserverEntry>} entries The entries to sort.
|
||
|
* @return {Array<IntersectionObserverEntry>} The sorted array.
|
||
|
*/
|
||
|
function sortRecords(entries) {
|
||
|
if (supportsNativeIntersectionObserver()) {
|
||
|
entries = entries.sort(function(a, b) {
|
||
|
return a.target.id < b.target.id ? -1 : 1;
|
||
|
});
|
||
|
}
|
||
|
return entries;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Adds the common styles used by all tests to the page.
|
||
|
*/
|
||
|
function addStyles() {
|
||
|
var styles = document.createElement('style');
|
||
|
styles.id = 'styles';
|
||
|
document.documentElement.appendChild(styles);
|
||
|
|
||
|
var cssText =
|
||
|
'#root {' +
|
||
|
' position: relative;' +
|
||
|
' width: 400px;' +
|
||
|
' height: 200px;' +
|
||
|
' background: #eee' +
|
||
|
'}' +
|
||
|
'#grand-parent {' +
|
||
|
' position: relative;' +
|
||
|
' width: 200px;' +
|
||
|
' height: 200px;' +
|
||
|
'}' +
|
||
|
'#parent {' +
|
||
|
' position: absolute;' +
|
||
|
' top: 0px;' +
|
||
|
' left: 200px;' +
|
||
|
' overflow: hidden;' +
|
||
|
' width: 200px;' +
|
||
|
' height: 200px;' +
|
||
|
' background: #ddd;' +
|
||
|
'}' +
|
||
|
'#target1, #target2, #target3, #target4, #target5 {' +
|
||
|
' position: absolute;' +
|
||
|
' top: 0px;' +
|
||
|
' left: 0px;' +
|
||
|
' width: 20px;' +
|
||
|
' height: 20px;' +
|
||
|
' transform: translateX(0px) translateY(0px);' +
|
||
|
' transition: transform .5s;' +
|
||
|
' background: #f00;' +
|
||
|
'}';
|
||
|
|
||
|
// IE8 doesn't allow setting innerHTML on a <style> element.
|
||
|
if (styles.styleSheet) {
|
||
|
styles.styleSheet.cssText = cssText;
|
||
|
}
|
||
|
else {
|
||
|
styles.innerHTML = cssText;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Adds the DOM fixtures used by all tests to the page and assigns them to
|
||
|
* global variables so they can be referenced within the tests.
|
||
|
*/
|
||
|
function addFixtures() {
|
||
|
var fixtures = document.createElement('div');
|
||
|
fixtures.id = 'fixtures';
|
||
|
|
||
|
fixtures.innerHTML =
|
||
|
'<div id="root">' +
|
||
|
' <div id="grand-parent">' +
|
||
|
' <div id="parent">' +
|
||
|
' <div id="target1"></div>' +
|
||
|
' <div id="target2"></div>' +
|
||
|
' <div id="target3"></div>' +
|
||
|
' <div id="target4"></div>' +
|
||
|
' </div>' +
|
||
|
' </div>' +
|
||
|
'</div>';
|
||
|
|
||
|
document.body.appendChild(fixtures);
|
||
|
|
||
|
rootEl = document.getElementById('root');
|
||
|
grandParentEl = document.getElementById('grand-parent');
|
||
|
parentEl = document.getElementById('parent');
|
||
|
targetEl1 = document.getElementById('target1');
|
||
|
targetEl2 = document.getElementById('target2');
|
||
|
targetEl3 = document.getElementById('target3');
|
||
|
targetEl4 = document.getElementById('target4');
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Removes the common styles from the page.
|
||
|
*/
|
||
|
function removeStyles() {
|
||
|
var styles = document.getElementById('styles');
|
||
|
styles.parentNode.removeChild(styles);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Removes the DOM fixtures from the page and resets the global references.
|
||
|
*/
|
||
|
function removeFixtures() {
|
||
|
var fixtures = document.getElementById('fixtures');
|
||
|
fixtures.parentNode.removeChild(fixtures);
|
||
|
|
||
|
rootEl = null;
|
||
|
grandParentEl = null;
|
||
|
parentEl = null;
|
||
|
targetEl1 = null;
|
||
|
targetEl2 = null;
|
||
|
targetEl3 = null;
|
||
|
targetEl4 = null;
|
||
|
}
|