Bug 1686668 [wpt PR 27185] - Update wpt metadata, a=testonly
[gecko.git] / dom / base / test / test_intersectionobservers.html
blobd228dfc9440c259a957b6498eca79032eb3ef8c2
1 <!DOCTYPE HTML>
2 <html>
3 <!--
4 https://bugzilla.mozilla.org/show_bug.cgi?id=1243846
6 Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html
8 Original license header:
10 Copyright 2016 Google Inc. All Rights Reserved.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
14 http://www.apache.org/licenses/LICENSE-2.0
15 Unless required by applicable law or agreed to in writing, software
16 distributed under the License is distributed on an "AS IS" BASIS,
17 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 See the License for the specific language governing permissions and
19 limitations under the License.
20 -->
21 <head>
22 <meta charset="utf-8">
23 <title>Test for Bug 1243846</title>
24 <script src="/tests/SimpleTest/SimpleTest.js"></script>
25 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
26 </head>
27 <body onload="onLoad()">
28 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1243846">Mozilla Bug 1243846</a>
29 <p id="display"></p>
30 <pre id="test">
31 <script type="application/javascript">
32 /* eslint "no-shadow": ["error", {"allow": ["done", "next"]}] */
33 var tests = [];
34 var curDescribeMsg = '';
35 var curItMsg = '';
37 function beforeEach_fn() { };
38 function afterEach_fn() { };
40 function before(fn) {
41 fn();
44 function beforeEach(fn) {
45 beforeEach_fn = fn;
48 function afterEach(fn) {
49 afterEach_fn = fn;
52 function it(msg, fn) {
53 tests.push({
54 msg: `${msg} [${curDescribeMsg}]`,
56 });
59 var callbacks = [];
60 function callDelayed(fn) {
61 callbacks.push(fn);
64 requestAnimationFrame(function tick() {
65 var i = callbacks.length;
66 while (i--) {
67 var cb = callbacks[i];
68 SimpleTest.executeSoon(function() { SimpleTest.executeSoon(cb) });
69 callbacks.splice(i, 1);
71 requestAnimationFrame(tick);
72 });
74 function expect(val) {
75 return {
76 to: {
77 throwException (regexp) {
78 try {
79 val();
80 ok(false, `${curItMsg} - an exception should have beeen thrown`);
81 } catch (e) {
82 ok(regexp.test(e), `${curItMsg} - supplied regexp should match thrown exception`);
85 get be() {
86 var fn = function (expected) {
87 is(val, expected, curItMsg);
89 fn.ok = function () {
90 ok(val, curItMsg);
92 fn.greaterThan = function (other) {
93 ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
95 fn.lessThan = function (other) {
96 ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
98 return fn;
100 eql (expected) {
101 if (Array.isArray(expected)) {
102 if (!Array.isArray(val)) {
103 ok(false, curItMsg, `${curItMsg} - should be an array,`);
104 return;
106 is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
107 if (expected.length != val.length) {
108 return;
110 for (var i = 0; i < expected.length; i++) {
111 is(val[i], expected[i], `${curItMsg} - array elements at position ${i} should be equal`);
112 if (expected[i] != val[i]) {
113 return;
116 ok(true);
123 function describe(msg, fn) {
124 curDescribeMsg = msg;
125 fn();
126 curDescribeMsg = '';
129 function next() {
130 var test = tests.shift();
131 if (test) {
132 console.log(test.msg);
133 curItMsg = test.msg;
134 var fn = test.fn;
135 beforeEach_fn();
136 if (fn.length) {
137 fn(function () {
138 afterEach_fn();
139 next();
141 } else {
142 fn();
143 afterEach_fn();
144 next();
146 } else {
147 SimpleTest.finish();
151 var sinon = {
152 spy () {
153 var cbs = [];
154 var fn = function () {
155 fn.callCount++;
156 fn.lastCall = { args: arguments };
157 if (cbs.length) {
158 cbs.shift()();
161 fn.callCount = 0;
162 fn.lastCall = { args: [] };
163 fn.waitForNotification = (fn1) => {
164 cbs.push(fn1);
166 return fn;
170 var ASYNC_TIMEOUT = 300;
173 var io;
174 var noop = function() {};
177 // References to DOM elements, which are accessible to any test
178 // and reset prior to each test so state isn't shared.
179 var rootEl;
180 var grandParentEl;
181 var parentEl;
182 var targetEl1;
183 var targetEl2;
184 var targetEl3;
185 var targetEl4;
186 var targetEl5;
189 describe('IntersectionObserver', function() {
191 before(function() {
196 beforeEach(function() {
197 addStyles();
198 addFixtures();
202 afterEach(function() {
203 if (io && 'disconnect' in io) io.disconnect();
204 io = null;
206 window.onmessage = null;
208 removeStyles();
209 removeFixtures();
213 describe('constructor', function() {
215 it('throws when callback is not a function', function() {
216 expect(function() {
217 io = new IntersectionObserver(null);
218 }).to.throwException(/.*/i);
222 it('instantiates root correctly', function() {
223 io = new IntersectionObserver(noop);
224 expect(io.root).to.be(null);
226 io = new IntersectionObserver(noop, {root: rootEl});
227 expect(io.root).to.be(rootEl);
231 it('throws when root is not an Element', function() {
232 expect(function() {
233 io = new IntersectionObserver(noop, {root: 'foo'});
234 }).to.throwException(/.*/i);
238 it('instantiates rootMargin correctly', function() {
239 io = new IntersectionObserver(noop, {rootMargin: '10px'});
240 expect(io.rootMargin).to.be('10px 10px 10px 10px');
242 io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
243 expect(io.rootMargin).to.be('10px -5% 10px -5%');
245 io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
246 expect(io.rootMargin).to.be('10px 20% 0px 20%');
248 io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
249 expect(io.rootMargin).to.be('0px 0px -5% 5px');
253 it('throws when rootMargin is not in pixels or percent', function() {
254 expect(function() {
255 io = new IntersectionObserver(noop, {rootMargin: 'auto'});
256 }).to.throwException(/pixels.*percent/i);
260 it('instantiates thresholds correctly', function() {
261 io = new IntersectionObserver(noop);
262 expect(io.thresholds).to.eql([0]);
264 io = new IntersectionObserver(noop, {threshold: 0.5});
265 expect(io.thresholds).to.eql([0.5]);
267 io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
268 expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
270 io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
271 expect(io.thresholds).to.eql([0, .5, 1]);
274 it('throws when a threshold value is not between 0 and 1', function() {
275 expect(function() {
276 io = new IntersectionObserver(noop, {threshold: [0, -1]});
277 }).to.throwException(/threshold/i);
280 it('throws when a threshold value is not a number', function() {
281 expect(function() {
282 io = new IntersectionObserver(noop, {threshold: "foo"});
283 }).to.throwException(/.*/i);
289 describe('observe', function() {
291 it('throws when target is not an Element', function() {
292 expect(function() {
293 io = new IntersectionObserver(noop);
294 io.observe(null);
295 }).to.throwException(/.*/i);
299 it('triggers if target intersects when observing begins', function(done) {
300 io = new IntersectionObserver(function(records) {
301 expect(records.length).to.be(1);
302 expect(records[0].intersectionRatio).to.be(1);
303 done();
304 }, {root: rootEl});
305 io.observe(targetEl1);
309 it('triggers with the correct arguments', function(done) {
310 io = new IntersectionObserver(function(records, observer) {
311 expect(records.length).to.be(1);
312 expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
313 expect(observer).to.be(io);
314 expect(this).to.be(io);
315 done();
316 }, {root: rootEl});
317 io.observe(targetEl1);
321 it('does trigger if target does not intersect when observing begins',
322 function(done) {
324 var spy = sinon.spy();
325 io = new IntersectionObserver(spy, {root: rootEl});
327 targetEl2.style.top = '-40px';
328 io.observe(targetEl2);
329 callDelayed(function() {
330 expect(spy.callCount).to.be(1);
331 done();
336 it('triggers if target or root becomes invisible',
337 function(done) {
339 var spy = sinon.spy();
340 io = new IntersectionObserver(spy, {root: rootEl});
342 runSequence([
343 function(done) {
344 io.observe(targetEl1);
345 spy.waitForNotification(function() {
346 expect(spy.callCount).to.be(1);
347 var records = sortRecords(spy.lastCall.args[0]);
348 expect(records.length).to.be(1);
349 expect(records[0].intersectionRatio).to.be(1);
350 done();
353 function(done) {
354 targetEl1.style.display = 'none';
355 spy.waitForNotification(function() {
356 expect(spy.callCount).to.be(2);
357 var records = sortRecords(spy.lastCall.args[0]);
358 expect(records.length).to.be(1);
359 expect(records[0].intersectionRatio).to.be(0);
360 done();
363 function(done) {
364 targetEl1.style.display = 'block';
365 spy.waitForNotification(function() {
366 expect(spy.callCount).to.be(3);
367 var records = sortRecords(spy.lastCall.args[0]);
368 expect(records.length).to.be(1);
369 expect(records[0].intersectionRatio).to.be(1);
370 done();
373 function(done) {
374 rootEl.style.display = 'none';
375 spy.waitForNotification(function() {
376 expect(spy.callCount).to.be(4);
377 var records = sortRecords(spy.lastCall.args[0]);
378 expect(records.length).to.be(1);
379 expect(records[0].intersectionRatio).to.be(0);
380 done();
383 function(done) {
384 rootEl.style.display = 'block';
385 spy.waitForNotification(function() {
386 expect(spy.callCount).to.be(5);
387 var records = sortRecords(spy.lastCall.args[0]);
388 expect(records.length).to.be(1);
389 expect(records[0].intersectionRatio).to.be(1);
390 done();
393 ], done);
397 it('handles container elements with non-visible overflow',
398 function(done) {
400 var spy = sinon.spy();
401 io = new IntersectionObserver(spy, {root: rootEl});
403 runSequence([
404 function(done) {
405 io.observe(targetEl1);
406 spy.waitForNotification(function() {
407 expect(spy.callCount).to.be(1);
408 var records = sortRecords(spy.lastCall.args[0]);
409 expect(records.length).to.be(1);
410 expect(records[0].intersectionRatio).to.be(1);
411 done();
414 function(done) {
415 targetEl1.style.left = '-40px';
416 spy.waitForNotification(function() {
417 expect(spy.callCount).to.be(2);
418 var records = sortRecords(spy.lastCall.args[0]);
419 expect(records.length).to.be(1);
420 expect(records[0].intersectionRatio).to.be(0);
421 done();
424 function(done) {
425 parentEl.style.overflow = 'visible';
426 spy.waitForNotification(function() {
427 expect(spy.callCount).to.be(3);
428 var records = sortRecords(spy.lastCall.args[0]);
429 expect(records.length).to.be(1);
430 expect(records[0].intersectionRatio).to.be(1);
431 done();
434 ], done);
438 it('observes one target at a single threshold correctly', function(done) {
440 var spy = sinon.spy();
441 io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
443 runSequence([
444 function(done) {
445 targetEl1.style.left = '-5px';
446 io.observe(targetEl1);
447 spy.waitForNotification(function() {
448 expect(spy.callCount).to.be(1);
449 var records = sortRecords(spy.lastCall.args[0]);
450 expect(records.length).to.be(1);
451 expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
452 done();
455 function(done) {
456 targetEl1.style.left = '-15px';
457 spy.waitForNotification(function() {
458 expect(spy.callCount).to.be(2);
459 var records = sortRecords(spy.lastCall.args[0]);
460 expect(records.length).to.be(1);
461 expect(records[0].intersectionRatio).to.be.lessThan(0.5);
462 done();
465 function(done) {
466 targetEl1.style.left = '-25px';
467 callDelayed(function() {
468 expect(spy.callCount).to.be(2);
469 done();
472 function(done) {
473 targetEl1.style.left = '-10px';
474 spy.waitForNotification(function() {
475 expect(spy.callCount).to.be(3);
476 var records = sortRecords(spy.lastCall.args[0]);
477 expect(records.length).to.be(1);
478 expect(records[0].intersectionRatio).to.be(0.5);
479 done();
482 ], done);
487 it('observes multiple targets at multiple thresholds correctly',
488 function(done) {
490 var spy = sinon.spy();
491 io = new IntersectionObserver(spy, {
492 root: rootEl,
493 threshold: [1, 0.5, 0]
496 runSequence([
497 function(done) {
498 targetEl1.style.top = '0px';
499 targetEl1.style.left = '-15px';
500 targetEl2.style.top = '-5px';
501 targetEl2.style.left = '0px';
502 targetEl3.style.top = '0px';
503 targetEl3.style.left = '205px';
504 io.observe(targetEl1);
505 io.observe(targetEl2);
506 io.observe(targetEl3);
507 spy.waitForNotification(function() {
508 expect(spy.callCount).to.be(1);
509 var records = sortRecords(spy.lastCall.args[0]);
510 expect(records.length).to.be(3);
511 expect(records[0].target).to.be(targetEl1);
512 expect(records[0].intersectionRatio).to.be(0.25);
513 expect(records[1].target).to.be(targetEl2);
514 expect(records[1].intersectionRatio).to.be(0.75);
515 done();
518 function(done) {
519 targetEl1.style.top = '0px';
520 targetEl1.style.left = '-5px';
521 targetEl2.style.top = '-15px';
522 targetEl2.style.left = '0px';
523 targetEl3.style.top = '0px';
524 targetEl3.style.left = '195px';
525 spy.waitForNotification(function() {
526 expect(spy.callCount).to.be(2);
527 var records = sortRecords(spy.lastCall.args[0]);
528 expect(records.length).to.be(3);
529 expect(records[0].target).to.be(targetEl1);
530 expect(records[0].intersectionRatio).to.be(0.75);
531 expect(records[1].target).to.be(targetEl2);
532 expect(records[1].intersectionRatio).to.be(0.25);
533 expect(records[2].target).to.be(targetEl3);
534 expect(records[2].intersectionRatio).to.be(0.25);
535 done();
538 function(done) {
539 targetEl1.style.top = '0px';
540 targetEl1.style.left = '5px';
541 targetEl2.style.top = '-25px';
542 targetEl2.style.left = '0px';
543 targetEl3.style.top = '0px';
544 targetEl3.style.left = '185px';
545 spy.waitForNotification(function() {
546 expect(spy.callCount).to.be(3);
547 var records = sortRecords(spy.lastCall.args[0]);
548 expect(records.length).to.be(3);
549 expect(records[0].target).to.be(targetEl1);
550 expect(records[0].intersectionRatio).to.be(1);
551 expect(records[1].target).to.be(targetEl2);
552 expect(records[1].intersectionRatio).to.be(0);
553 expect(records[2].target).to.be(targetEl3);
554 expect(records[2].intersectionRatio).to.be(0.75);
555 done();
558 function(done) {
559 targetEl1.style.top = '0px';
560 targetEl1.style.left = '15px';
561 targetEl2.style.top = '-35px';
562 targetEl2.style.left = '0px';
563 targetEl3.style.top = '0px';
564 targetEl3.style.left = '175px';
565 spy.waitForNotification(function() {
566 expect(spy.callCount).to.be(4);
567 var records = sortRecords(spy.lastCall.args[0]);
568 expect(records.length).to.be(1);
569 expect(records[0].target).to.be(targetEl3);
570 expect(records[0].intersectionRatio).to.be(1);
571 done();
574 ], done);
578 it('handles rootMargin properly', function(done) {
580 parentEl.style.overflow = 'visible';
581 targetEl1.style.top = '0px';
582 targetEl1.style.left = '-20px';
583 targetEl2.style.top = '-20px';
584 targetEl2.style.left = '0px';
585 targetEl3.style.top = '0px';
586 targetEl3.style.left = '200px';
587 targetEl4.style.top = '180px';
588 targetEl4.style.left = '180px';
590 runSequence([
591 function(done) {
592 io = new IntersectionObserver(function(records) {
593 records = sortRecords(records);
594 expect(records.length).to.be(4);
595 expect(records[0].target).to.be(targetEl1);
596 expect(records[0].intersectionRatio).to.be(1);
597 expect(records[1].target).to.be(targetEl2);
598 expect(records[1].intersectionRatio).to.be(.5);
599 expect(records[2].target).to.be(targetEl3);
600 expect(records[2].intersectionRatio).to.be(.5);
601 expect(records[3].target).to.be(targetEl4);
602 expect(records[3].intersectionRatio).to.be(1);
603 io.disconnect();
604 done();
605 }, {root: rootEl, rootMargin: '10px'});
607 io.observe(targetEl1);
608 io.observe(targetEl2);
609 io.observe(targetEl3);
610 io.observe(targetEl4);
612 function(done) {
613 io = new IntersectionObserver(function(records) {
614 records = sortRecords(records);
615 expect(records.length).to.be(4);
616 expect(records[0].target).to.be(targetEl1);
617 expect(records[0].intersectionRatio).to.be(0.5);
618 expect(records[2].target).to.be(targetEl3);
619 expect(records[2].intersectionRatio).to.be(0.5);
620 expect(records[3].target).to.be(targetEl4);
621 expect(records[3].intersectionRatio).to.be(0.5);
622 io.disconnect();
623 done();
624 }, {root: rootEl, rootMargin: '-10px 10%'});
626 io.observe(targetEl1);
627 io.observe(targetEl2);
628 io.observe(targetEl3);
629 io.observe(targetEl4);
631 function(done) {
632 io = new IntersectionObserver(function(records) {
633 records = sortRecords(records);
634 expect(records.length).to.be(4);
635 expect(records[0].target).to.be(targetEl1);
636 expect(records[0].intersectionRatio).to.be(0.5);
637 expect(records[3].target).to.be(targetEl4);
638 expect(records[3].intersectionRatio).to.be(0.5);
639 io.disconnect();
640 done();
641 }, {root: rootEl, rootMargin: '-5% -2.5% 0px'});
643 io.observe(targetEl1);
644 io.observe(targetEl2);
645 io.observe(targetEl3);
646 io.observe(targetEl4);
648 function(done) {
649 io = new IntersectionObserver(function(records) {
650 records = sortRecords(records);
651 expect(records.length).to.be(4);
652 expect(records[0].target).to.be(targetEl1);
653 expect(records[0].intersectionRatio).to.be(0.5);
654 expect(records[1].target).to.be(targetEl2);
655 expect(records[1].intersectionRatio).to.be(0.5);
656 expect(records[3].target).to.be(targetEl4);
657 expect(records[3].intersectionRatio).to.be(0.25);
658 io.disconnect();
659 done();
660 }, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});
662 io.observe(targetEl1);
663 io.observe(targetEl2);
664 io.observe(targetEl3);
665 io.observe(targetEl4);
667 ], done);
671 it('handles targets on the boundary of root', function(done) {
673 var spy = sinon.spy();
674 io = new IntersectionObserver(spy, {root: rootEl});
676 runSequence([
677 function(done) {
678 targetEl1.style.top = '0px';
679 targetEl1.style.left = '-21px';
680 targetEl2.style.top = '-20px';
681 targetEl2.style.left = '0px';
682 io.observe(targetEl1);
683 io.observe(targetEl2);
684 spy.waitForNotification(function() {
685 expect(spy.callCount).to.be(1);
686 var records = sortRecords(spy.lastCall.args[0]);
687 expect(records.length).to.be(2);
688 expect(records[1].intersectionRatio).to.be(0);
689 expect(records[1].target).to.be(targetEl2);
690 done();
693 function(done) {
694 targetEl1.style.top = '0px';
695 targetEl1.style.left = '-20px';
696 targetEl2.style.top = '-21px';
697 targetEl2.style.left = '0px';
698 spy.waitForNotification(function() {
699 expect(spy.callCount).to.be(2);
700 var records = sortRecords(spy.lastCall.args[0]);
701 expect(records.length).to.be(2);
702 expect(records[0].intersectionRatio).to.be(0);
703 expect(records[0].isIntersecting).to.be.ok();
704 expect(records[0].target).to.be(targetEl1);
705 expect(records[1].intersectionRatio).to.be(0);
706 expect(records[1].target).to.be(targetEl2);
707 done();
710 function(done) {
711 targetEl1.style.top = '-20px';
712 targetEl1.style.left = '200px';
713 targetEl2.style.top = '200px';
714 targetEl2.style.left = '200px';
715 spy.waitForNotification(function() {
716 expect(spy.callCount).to.be(3);
717 var records = sortRecords(spy.lastCall.args[0]);
718 expect(records.length).to.be(1);
719 expect(records[0].intersectionRatio).to.be(0);
720 expect(records[0].target).to.be(targetEl2);
721 done();
724 function(done) {
725 targetEl3.style.top = '20px';
726 targetEl3.style.left = '-20px';
727 targetEl4.style.top = '-20px';
728 targetEl4.style.left = '20px';
729 io.observe(targetEl3);
730 io.observe(targetEl4);
731 spy.waitForNotification(function() {
732 expect(spy.callCount).to.be(4);
733 var records = sortRecords(spy.lastCall.args[0]);
734 expect(records.length).to.be(2);
735 expect(records[0].intersectionRatio).to.be(0);
736 expect(records[0].isIntersecting).to.be.ok();
737 expect(records[0].target).to.be(targetEl3);
738 expect(records[1].intersectionRatio).to.be(0);
739 expect(records[1].target).to.be(targetEl4);
740 done();
743 ], done);
748 it('handles zero-size targets within the root coordinate space',
749 function(done) {
751 var spy = sinon.spy();
752 io = new IntersectionObserver(spy, {root: rootEl});
754 runSequence([
755 function(done) {
756 targetEl1.style.top = '0px';
757 targetEl1.style.left = '0px';
758 targetEl1.style.width = '0px';
759 targetEl1.style.height = '0px';
760 io.observe(targetEl1);
761 spy.waitForNotification(function() {
762 var records = sortRecords(spy.lastCall.args[0]);
763 expect(records.length).to.be(1);
764 expect(records[0].intersectionRatio).to.be(1);
765 expect(records[0].isIntersecting).to.be.ok();
766 done();
769 function(done) {
770 targetEl1.style.top = '-1px';
771 spy.waitForNotification(function() {
772 var records = sortRecords(spy.lastCall.args[0]);
773 expect(records.length).to.be(1);
774 expect(records[0].intersectionRatio).to.be(0);
775 expect(records[0].isIntersecting).to.be(false);
776 done();
779 ], done);
783 it('handles root/target elements not yet in the DOM', function(done) {
785 rootEl.remove();
786 targetEl1.remove();
788 var spy = sinon.spy();
789 io = new IntersectionObserver(spy, {root: rootEl});
791 runSequence([
792 function(done) {
793 io.observe(targetEl1);
794 callDelayed(done);
796 function(done) {
797 document.getElementById('fixtures').appendChild(rootEl);
798 callDelayed(function() {
799 expect(spy.callCount).to.be(1);
800 done();
803 function(done) {
804 parentEl.insertBefore(targetEl1, targetEl2);
805 spy.waitForNotification(function() {
806 expect(spy.callCount).to.be(2);
807 var records = sortRecords(spy.lastCall.args[0]);
808 expect(records.length).to.be(1);
809 expect(records[0].intersectionRatio).to.be(1);
810 expect(records[0].target).to.be(targetEl1);
811 done();
814 function(done) {
815 grandParentEl.remove();
816 spy.waitForNotification(function() {
817 expect(spy.callCount).to.be(3);
818 var records = sortRecords(spy.lastCall.args[0]);
819 expect(records.length).to.be(1);
820 expect(records[0].intersectionRatio).to.be(0);
821 expect(records[0].target).to.be(targetEl1);
822 done();
825 function(done) {
826 rootEl.appendChild(targetEl1);
827 spy.waitForNotification(function() {
828 expect(spy.callCount).to.be(4);
829 var records = sortRecords(spy.lastCall.args[0]);
830 expect(records.length).to.be(1);
831 expect(records[0].intersectionRatio).to.be(1);
832 expect(records[0].target).to.be(targetEl1);
833 done();
836 function(done) {
837 rootEl.remove();
838 spy.waitForNotification(function() {
839 expect(spy.callCount).to.be(5);
840 var records = sortRecords(spy.lastCall.args[0]);
841 expect(records.length).to.be(1);
842 expect(records[0].intersectionRatio).to.be(0);
843 expect(records[0].target).to.be(targetEl1);
844 done();
847 ], done);
851 it('handles sub-root element scrolling', function(done) {
852 io = new IntersectionObserver(function(records) {
853 expect(records.length).to.be(1);
854 expect(records[0].intersectionRatio).to.be(1);
855 done();
856 }, {root: rootEl});
858 io.observe(targetEl3);
859 callDelayed(function() {
860 parentEl.scrollLeft = 40;
865 it('supports CSS transitions and transforms', function(done) {
867 targetEl1.style.top = '220px';
868 targetEl1.style.left = '220px';
870 var callCount = 0;
872 io = new IntersectionObserver(function(records) {
873 callCount++;
874 if (callCount <= 1) {
875 return;
877 expect(records.length).to.be(1);
878 expect(records[0].intersectionRatio).to.be(1);
879 done();
880 }, {root: rootEl, threshold: [1]});
882 io.observe(targetEl1);
883 callDelayed(function() {
884 targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
889 it('uses the viewport when no root is specified', function(done) {
890 window.onmessage = function (e) {
891 expect(e.data).to.be.ok();
892 win.close();
893 done();
896 var win = window.open("intersectionobserver_window.html");
899 it('triggers only once if observed multiple times (and does not crash when collected)', function(done) {
900 var spy = sinon.spy();
901 io = new IntersectionObserver(spy, {root: rootEl});
902 io.observe(targetEl1);
903 io.observe(targetEl1);
904 io.observe(targetEl1);
906 spy.waitForNotification(function() {
907 callDelayed(function () {
908 expect(spy.callCount).to.be(1);
909 done();
916 describe('observe subframe', function () {
918 it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
919 function(done) {
921 io = new IntersectionObserver(function(records) {
922 expect(records.length).to.be(1);
923 expect(records[0].boundingClientRect.top, targetEl5.getBoundingClientRect().top);
924 expect(records[0].boundingClientRect.left, targetEl5.getBoundingClientRect().left);
925 expect(records[0].boundingClientRect.width, targetEl5.getBoundingClientRect().width);
926 expect(records[0].boundingClientRect.height, targetEl5.getBoundingClientRect().height);
927 done();
928 }, {threshold: [1]});
930 targetEl4.onload = function () {
931 targetEl5 = targetEl4.contentDocument.getElementById('target5');
932 io.observe(targetEl5);
935 targetEl4.src = "intersectionobserver_iframe.html";
938 it('rootBounds is set to null for cross-origin observations', function(done) {
940 window.onmessage = function (e) {
941 expect(e.data).to.be(true);
942 done();
945 targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_cross_domain_iframe.html";
951 describe('takeRecords', function() {
953 it('supports getting records before the callback is invoked', function(done) {
955 var lastestRecords = [];
956 io = new IntersectionObserver(function(records) {
957 lastestRecords = lastestRecords.concat(records);
958 }, {root: rootEl});
959 io.observe(targetEl1);
961 window.requestAnimationFrame && requestAnimationFrame(function wait() {
962 lastestRecords = lastestRecords.concat(io.takeRecords());
963 if (!lastestRecords.length) {
964 requestAnimationFrame(wait);
965 return;
967 callDelayed(function() {
968 expect(lastestRecords.length).to.be(1);
969 expect(lastestRecords[0].intersectionRatio).to.be(1);
970 done();
978 describe('unobserve', function() {
980 it('removes targets from the internal store', function(done) {
982 var spy = sinon.spy();
983 io = new IntersectionObserver(spy, {root: rootEl});
985 runSequence([
986 function(done) {
987 targetEl1.style.top = targetEl2.style.top = '0px';
988 targetEl1.style.left = targetEl2.style.left = '0px';
989 io.observe(targetEl1);
990 io.observe(targetEl2);
991 spy.waitForNotification(function() {
992 expect(spy.callCount).to.be(1);
993 var records = sortRecords(spy.lastCall.args[0]);
994 expect(records.length).to.be(2);
995 expect(records[0].target).to.be(targetEl1);
996 expect(records[0].intersectionRatio).to.be(1);
997 expect(records[1].target).to.be(targetEl2);
998 expect(records[1].intersectionRatio).to.be(1);
999 done();
1002 function(done) {
1003 io.unobserve(targetEl1);
1004 targetEl1.style.top = targetEl2.style.top = '0px';
1005 targetEl1.style.left = targetEl2.style.left = '-40px';
1006 spy.waitForNotification(function() {
1007 expect(spy.callCount).to.be(2);
1008 var records = sortRecords(spy.lastCall.args[0]);
1009 expect(records.length).to.be(1);
1010 expect(records[0].target).to.be(targetEl2);
1011 expect(records[0].intersectionRatio).to.be(0);
1012 done();
1015 function(done) {
1016 io.unobserve(targetEl2);
1017 targetEl1.style.top = targetEl2.style.top = '0px';
1018 targetEl1.style.left = targetEl2.style.left = '0px';
1019 callDelayed(function() {
1020 expect(spy.callCount).to.be(2);
1021 done();
1024 ], done);
1030 describe('disconnect', function() {
1032 it('removes all targets and stops listening for changes', function(done) {
1034 var spy = sinon.spy();
1035 io = new IntersectionObserver(spy, {root: rootEl});
1037 runSequence([
1038 function(done) {
1039 targetEl1.style.top = targetEl2.style.top = '0px';
1040 targetEl1.style.left = targetEl2.style.left = '0px';
1041 io.observe(targetEl1);
1042 io.observe(targetEl2);
1043 spy.waitForNotification(function() {
1044 expect(spy.callCount).to.be(1);
1045 var records = sortRecords(spy.lastCall.args[0]);
1046 expect(records.length).to.be(2);
1047 expect(records[0].target).to.be(targetEl1);
1048 expect(records[0].intersectionRatio).to.be(1);
1049 expect(records[1].target).to.be(targetEl2);
1050 expect(records[1].intersectionRatio).to.be(1);
1051 done();
1054 function(done) {
1055 io.disconnect();
1056 targetEl1.style.top = targetEl2.style.top = '0px';
1057 targetEl1.style.left = targetEl2.style.left = '-40px';
1058 callDelayed(function() {
1059 expect(spy.callCount).to.be(1);
1060 done();
1063 ], done);
1073 * Runs a sequence of function and when finished invokes the done callback.
1074 * Each function in the sequence is invoked with its own done function and
1075 * it should call that function once it's complete.
1076 * @param {Array<Function>} functions An array of async functions.
1077 * @param {Function} done A final callback to be invoked once all function
1078 * have run.
1080 function runSequence(functions, done) {
1081 var next = functions.shift();
1082 if (next) {
1083 next(function() {
1084 runSequence(functions, done);
1086 } else {
1087 done && done();
1093 * Sorts an array of records alphebetically by ascending ID. Since the current
1094 * native implementation doesn't sort change entries by `observe` order, we do
1095 * that ourselves for the non-polyfill case. Since all tests call observe
1096 * on targets in sequential order, this should always match.
1097 * https://crbug.com/613679
1098 * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
1099 * @return {Array<IntersectionObserverEntry>} The sorted array.
1101 function sortRecords(entries) {
1102 entries = entries.sort(function(a, b) {
1103 return a.target.id < b.target.id ? -1 : 1;
1105 return entries;
1110 * Adds the common styles used by all tests to the page.
1112 function addStyles() {
1113 var styles = document.createElement('style');
1114 styles.id = 'styles';
1115 document.documentElement.appendChild(styles);
1117 var cssText =
1118 '#root {' +
1119 ' position: relative;' +
1120 ' width: 400px;' +
1121 ' height: 200px;' +
1122 ' background: #eee' +
1123 '}' +
1124 '#grand-parent {' +
1125 ' position: relative;' +
1126 ' width: 200px;' +
1127 ' height: 200px;' +
1128 '}' +
1129 '#parent {' +
1130 ' position: absolute;' +
1131 ' top: 0px;' +
1132 ' left: 200px;' +
1133 ' overflow: hidden;' +
1134 ' width: 200px;' +
1135 ' height: 200px;' +
1136 ' background: #ddd;' +
1137 '}' +
1138 '#target1, #target2, #target3, #target4 {' +
1139 ' position: absolute;' +
1140 ' top: 0px;' +
1141 ' left: 0px;' +
1142 ' width: 20px;' +
1143 ' height: 20px;' +
1144 ' transform: translateX(0px) translateY(0px);' +
1145 ' transition: transform .5s;' +
1146 ' background: #f00;' +
1147 ' border: none;' +
1148 '}';
1150 styles.innerHTML = cssText;
1155 * Adds the DOM fixtures used by all tests to the page and assigns them to
1156 * global variables so they can be referenced within the tests.
1158 function addFixtures() {
1159 var fixtures = document.createElement('div');
1160 fixtures.id = 'fixtures';
1162 fixtures.innerHTML =
1163 '<div id="root">' +
1164 ' <div id="grand-parent">' +
1165 ' <div id="parent">' +
1166 ' <div id="target1"></div>' +
1167 ' <div id="target2"></div>' +
1168 ' <div id="target3"></div>' +
1169 ' <iframe id="target4"></iframe>' +
1170 ' </div>' +
1171 ' </div>' +
1172 '</div>';
1174 document.body.appendChild(fixtures);
1176 rootEl = document.getElementById('root');
1177 grandParentEl = document.getElementById('grand-parent');
1178 parentEl = document.getElementById('parent');
1179 targetEl1 = document.getElementById('target1');
1180 targetEl2 = document.getElementById('target2');
1181 targetEl3 = document.getElementById('target3');
1182 targetEl4 = document.getElementById('target4');
1187 * Removes the common styles from the page.
1189 function removeStyles() {
1190 var styles = document.getElementById('styles');
1191 styles.remove();
1196 * Removes the DOM fixtures from the page and resets the global references.
1198 function removeFixtures() {
1199 var fixtures = document.getElementById('fixtures');
1200 fixtures.remove();
1202 rootEl = null;
1203 grandParentEl = null;
1204 parentEl = null;
1205 targetEl1 = null;
1206 targetEl2 = null;
1207 targetEl3 = null;
1208 targetEl4 = null;
1211 function onLoad() {
1212 SpecialPowers.pushPrefEnv({"set": [["dom.IntersectionObserver.enabled", true]]}, next);
1215 SimpleTest.waitForExplicitFinish();
1216 </script>
1217 </pre>
1218 <div id="log">
1219 </div>
1220 </body>
1221 </html>