tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_intersectionobservers.html (39286B)


      1 <!DOCTYPE HTML>
      2 <html>
      3 <!--
      4 https://bugzilla.mozilla.org/show_bug.cgi?id=1243846
      5 
      6 Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html
      7 
      8 Original license header:
      9 
     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 = '';
     36 
     37  function beforeEach_fn() { };
     38  function afterEach_fn() { };
     39 
     40  function before(fn) {
     41    fn();
     42  }
     43 
     44  function beforeEach(fn) {
     45    beforeEach_fn = fn;
     46  }
     47 
     48  function afterEach(fn) {
     49    afterEach_fn = fn;
     50  }
     51 
     52  function it(msg, fn) {
     53    tests.push({
     54      msg: `${msg} [${curDescribeMsg}]`,
     55      fn
     56    });
     57  }
     58 
     59  var callbacks = [];
     60  function callDelayed(fn) {
     61    callbacks.push(fn);
     62  }
     63 
     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);
     70    }
     71    requestAnimationFrame(tick);
     72  });
     73 
     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`);
     83        }
     84      },
     85        get be() {
     86          var fn = function (expected) {
     87            is(val, expected, curItMsg);
     88          };
     89          fn.ok = function () {
     90            ok(val, curItMsg);
     91          };
     92          fn.greaterThan = function (other) {
     93            ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
     94          };
     95          fn.lessThan = function (other) {
     96            ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
     97          };
     98          return fn;
     99        },
    100        eql (expected) {
    101        if (Array.isArray(expected)) {
    102          if (!Array.isArray(val)) {
    103            ok(false, curItMsg, `${curItMsg} - should be an array,`);
    104            return;
    105          }
    106          is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
    107          if (expected.length != val.length) {
    108            return;
    109          }
    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;
    114            }
    115          }
    116          ok(true);
    117        }
    118      },
    119      }
    120    }
    121  }
    122 
    123  function describe(msg, fn) {
    124    curDescribeMsg = msg;
    125    fn();
    126    curDescribeMsg = '';
    127  }
    128 
    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();
    140        });
    141      } else {
    142        fn();
    143        afterEach_fn();
    144        next();
    145      }
    146    } else {
    147      SimpleTest.finish();
    148    }
    149  }
    150 
    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()();
    159      }
    160    };
    161    fn.callCount = 0;
    162    fn.lastCall = { args: [] };
    163    fn.waitForNotification = (fn1) => {
    164      cbs.push(fn1);
    165    };
    166    return fn;
    167  }
    168  };
    169 
    170  var ASYNC_TIMEOUT = 300;
    171 
    172 
    173  var io;
    174  var noop = function() {};
    175 
    176 
    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;
    187 
    188 
    189  describe('IntersectionObserver', function() {
    190 
    191    before(function() {
    192 
    193    });
    194 
    195 
    196    beforeEach(function() {
    197      addStyles();
    198      addFixtures();
    199    });
    200 
    201 
    202    afterEach(function() {
    203      if (io && 'disconnect' in io) io.disconnect();
    204      io = null;
    205 
    206      window.onmessage = null;
    207 
    208      removeStyles();
    209      removeFixtures();
    210    });
    211 
    212 
    213    describe('constructor', function() {
    214 
    215      it('throws when callback is not a function', function() {
    216        expect(function() {
    217          io = new IntersectionObserver(null);
    218        }).to.throwException(/.*/i);
    219      });
    220 
    221 
    222      it('instantiates root correctly', function() {
    223        io = new IntersectionObserver(noop);
    224        expect(io.root).to.be(null);
    225 
    226        io = new IntersectionObserver(noop, {root: rootEl});
    227        expect(io.root).to.be(rootEl);
    228      });
    229 
    230 
    231      it('throws when root is not an Element', function() {
    232        expect(function() {
    233          io = new IntersectionObserver(noop, {root: 'foo'});
    234        }).to.throwException(/.*/i);
    235      });
    236 
    237 
    238      it('instantiates rootMargin correctly', function() {
    239        io = new IntersectionObserver(noop, {rootMargin: '10px'});
    240        expect(io.rootMargin).to.be('10px 10px 10px 10px');
    241 
    242        io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
    243        expect(io.rootMargin).to.be('10px -5% 10px -5%');
    244 
    245        io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
    246        expect(io.rootMargin).to.be('10px 20% 0px 20%');
    247 
    248        io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
    249        expect(io.rootMargin).to.be('0px 0px -5% 5px');
    250      });
    251 
    252 
    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);
    257      });
    258 
    259 
    260      it('instantiates thresholds correctly', function() {
    261        io = new IntersectionObserver(noop);
    262        expect(io.thresholds).to.eql([0]);
    263 
    264        io = new IntersectionObserver(noop, {threshold: 0.5});
    265        expect(io.thresholds).to.eql([0.5]);
    266 
    267        io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
    268        expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
    269 
    270        io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
    271        expect(io.thresholds).to.eql([0, .5, 1]);
    272      });
    273 
    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);
    278      });
    279 
    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);
    284      });
    285 
    286    });
    287 
    288 
    289    describe('observe', function() {
    290 
    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);
    296      });
    297 
    298 
    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);
    306      });
    307 
    308 
    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);
    318      });
    319 
    320 
    321      it('does trigger if target does not intersect when observing begins',
    322          function(done) {
    323 
    324        var spy = sinon.spy();
    325        io = new IntersectionObserver(spy, {root: rootEl});
    326 
    327        targetEl2.style.top = '-40px';
    328        io.observe(targetEl2);
    329        callDelayed(function() {
    330          expect(spy.callCount).to.be(1);
    331          done();
    332        });
    333      });
    334 
    335 
    336      it('triggers if target or root becomes invisible',
    337          function(done) {
    338 
    339        var spy = sinon.spy();
    340        io = new IntersectionObserver(spy, {root: rootEl});
    341 
    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();
    351            });
    352          },
    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();
    361            });
    362          },
    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();
    371            });
    372          },
    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();
    381            });
    382          },
    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();
    391            });
    392          },
    393        ], done);
    394      });
    395 
    396 
    397      it('handles container elements with non-visible overflow',
    398          function(done) {
    399 
    400        var spy = sinon.spy();
    401        io = new IntersectionObserver(spy, {root: rootEl});
    402 
    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();
    412            });
    413          },
    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();
    422            });
    423          },
    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();
    432            });
    433          }
    434        ], done);
    435      });
    436 
    437 
    438      it('observes one target at a single threshold correctly', function(done) {
    439 
    440        var spy = sinon.spy();
    441        io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
    442 
    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();
    453            });
    454          },
    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();
    463            });
    464          },
    465          function(done) {
    466            targetEl1.style.left = '-25px';
    467            callDelayed(function() {
    468              expect(spy.callCount).to.be(2);
    469              done();
    470            });
    471          },
    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();
    480            });
    481          }
    482        ], done);
    483 
    484      });
    485 
    486 
    487      it('observes multiple targets at multiple thresholds correctly',
    488          function(done) {
    489 
    490        var spy = sinon.spy();
    491        io = new IntersectionObserver(spy, {
    492          root: rootEl,
    493          threshold: [1, 0.5, 0]
    494        });
    495 
    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();
    516            });
    517          },
    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();
    536            });
    537          },
    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();
    556            });
    557          },
    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();
    572            });
    573          }
    574        ], done);
    575      });
    576 
    577 
    578      it('handles rootMargin properly', function(done) {
    579 
    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';
    589 
    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'});
    606 
    607            io.observe(targetEl1);
    608            io.observe(targetEl2);
    609            io.observe(targetEl3);
    610            io.observe(targetEl4);
    611          },
    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%'});
    625 
    626            io.observe(targetEl1);
    627            io.observe(targetEl2);
    628            io.observe(targetEl3);
    629            io.observe(targetEl4);
    630          },
    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'});
    642 
    643            io.observe(targetEl1);
    644            io.observe(targetEl2);
    645            io.observe(targetEl3);
    646            io.observe(targetEl4);
    647          },
    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'});
    661 
    662            io.observe(targetEl1);
    663            io.observe(targetEl2);
    664            io.observe(targetEl3);
    665            io.observe(targetEl4);
    666          }
    667        ], done);
    668      });
    669 
    670 
    671      it('handles targets on the boundary of root', function(done) {
    672 
    673        var spy = sinon.spy();
    674        io = new IntersectionObserver(spy, {root: rootEl});
    675 
    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();
    691            });
    692          },
    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();
    708            });
    709          },
    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();
    722            });
    723          },
    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();
    741            });
    742          }
    743        ], done);
    744 
    745      });
    746 
    747 
    748      it('handles zero-size targets within the root coordinate space',
    749          function(done) {
    750 
    751        var spy = sinon.spy();
    752        io = new IntersectionObserver(spy, {root: rootEl});
    753 
    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();
    767            });
    768          },
    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();
    777            });
    778          }
    779        ], done);
    780      });
    781 
    782 
    783      it('handles root/target elements not yet in the DOM', function(done) {
    784 
    785        rootEl.remove();
    786        targetEl1.remove();
    787 
    788        var spy = sinon.spy();
    789        io = new IntersectionObserver(spy, {root: rootEl});
    790 
    791        runSequence([
    792          function(done) {
    793            io.observe(targetEl1);
    794            callDelayed(done);
    795          },
    796          function(done) {
    797            document.getElementById('fixtures').appendChild(rootEl);
    798            callDelayed(function() {
    799              expect(spy.callCount).to.be(1);
    800              done();
    801            });
    802          },
    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();
    812            });
    813          },
    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();
    823            });
    824          },
    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();
    834            });
    835          },
    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();
    845            });
    846          }
    847        ], done);
    848      });
    849 
    850 
    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});
    857 
    858        io.observe(targetEl3);
    859        callDelayed(function() {
    860          parentEl.scrollLeft = 40;
    861        });
    862      });
    863 
    864 
    865      it('supports CSS transitions and transforms', function(done) {
    866 
    867        targetEl1.style.top = '220px';
    868        targetEl1.style.left = '220px';
    869 
    870        var callCount = 0;
    871 
    872        io = new IntersectionObserver(function(records) {
    873          callCount++;
    874          if (callCount <= 1) {
    875            return;
    876          }
    877          expect(records.length).to.be(1);
    878          expect(records[0].intersectionRatio).to.be(1);
    879          done();
    880        }, {root: rootEl, threshold: [1]});
    881 
    882        io.observe(targetEl1);
    883        callDelayed(function() {
    884          targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
    885        });
    886      });
    887 
    888 
    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();
    894        };
    895 
    896        var win = window.open("intersectionobserver_window.html");
    897      });
    898 
    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);
    905 
    906        spy.waitForNotification(function() {
    907          callDelayed(function () {
    908            expect(spy.callCount).to.be(1);
    909            done();
    910          });
    911        });
    912      });
    913 
    914    });
    915 
    916    describe('observe subframe', function () {
    917 
    918      it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
    919          function(done) {
    920 
    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]});
    929 
    930        targetEl4.onload = function () {
    931          targetEl5 = targetEl4.contentDocument.getElementById('target5');
    932          io.observe(targetEl5);
    933        }
    934 
    935        targetEl4.src = "intersectionobserver_iframe.html";
    936      });
    937 
    938      it('rootBounds is set to null for cross-origin observations', function(done) {
    939 
    940        window.onmessage = function (e) {
    941          expect(e.data).to.be(true);
    942          done();
    943        };
    944 
    945        targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_cross_domain_iframe.html";
    946 
    947      });
    948 
    949    });
    950 
    951    describe('takeRecords', function() {
    952 
    953      it('supports getting records before the callback is invoked', function(done) {
    954 
    955        var lastestRecords = [];
    956        io = new IntersectionObserver(function(records) {
    957          lastestRecords = lastestRecords.concat(records);
    958        }, {root: rootEl});
    959        io.observe(targetEl1);
    960 
    961        window.requestAnimationFrame && requestAnimationFrame(function wait() {
    962          lastestRecords = lastestRecords.concat(io.takeRecords());
    963          if (!lastestRecords.length) {
    964            requestAnimationFrame(wait);
    965            return;
    966          }
    967          callDelayed(function() {
    968            expect(lastestRecords.length).to.be(1);
    969            expect(lastestRecords[0].intersectionRatio).to.be(1);
    970            done();
    971          });
    972        });
    973 
    974      });
    975 
    976    });
    977 
    978    describe('unobserve', function() {
    979 
    980      it('removes targets from the internal store', function(done) {
    981 
    982        var spy = sinon.spy();
    983        io = new IntersectionObserver(spy, {root: rootEl});
    984 
    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();
   1000            });
   1001          },
   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();
   1013            });
   1014          },
   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();
   1022            });
   1023          }
   1024        ], done);
   1025 
   1026      });
   1027 
   1028    });
   1029 
   1030    describe('disconnect', function() {
   1031 
   1032      it('removes all targets and stops listening for changes', function(done) {
   1033 
   1034        var spy = sinon.spy();
   1035        io = new IntersectionObserver(spy, {root: rootEl});
   1036 
   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();
   1052            });
   1053          },
   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();
   1061            });
   1062          }
   1063        ], done);
   1064 
   1065      });
   1066 
   1067    });
   1068 
   1069  });
   1070 
   1071 
   1072  /**
   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   *
   1077   * @param {Array<Function>} functions An array of async functions.
   1078   * @param {Function} done A final callback to be invoked once all function
   1079   *     have run.
   1080   */
   1081  function runSequence(functions, done) {
   1082    var next = functions.shift();
   1083    if (next) {
   1084      next(function() {
   1085        runSequence(functions, done);
   1086      });
   1087    } else {
   1088      done && done();
   1089    }
   1090  }
   1091 
   1092 
   1093  /**
   1094   * Sorts an array of records alphebetically by ascending ID. Since the current
   1095   * native implementation doesn't sort change entries by `observe` order, we do
   1096   * that ourselves for the non-polyfill case. Since all tests call observe
   1097   * on targets in sequential order, this should always match.
   1098   * https://crbug.com/613679
   1099   *
   1100   * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
   1101   * @return {Array<IntersectionObserverEntry>} The sorted array.
   1102   */
   1103  function sortRecords(entries) {
   1104    entries = entries.sort(function(a, b) {
   1105      return a.target.id < b.target.id ? -1 : 1;
   1106    });
   1107    return entries;
   1108  }
   1109 
   1110 
   1111  /**
   1112   * Adds the common styles used by all tests to the page.
   1113   */
   1114  function addStyles() {
   1115    var styles = document.createElement('style');
   1116    styles.id = 'styles';
   1117    document.documentElement.appendChild(styles);
   1118 
   1119    var cssText =
   1120        '#root {' +
   1121        '  position: relative;' +
   1122        '  width: 400px;' +
   1123        '  height: 200px;' +
   1124        '  background: #eee' +
   1125        '}' +
   1126        '#grand-parent {' +
   1127        '  position: relative;' +
   1128        '  width: 200px;' +
   1129        '  height: 200px;' +
   1130        '}' +
   1131        '#parent {' +
   1132        '  position: absolute;' +
   1133        '  top: 0px;' +
   1134        '  left: 200px;' +
   1135        '  overflow: hidden;' +
   1136        '  width: 200px;' +
   1137        '  height: 200px;' +
   1138        '  background: #ddd;' +
   1139        '}' +
   1140        '#target1, #target2, #target3, #target4 {' +
   1141        '  position: absolute;' +
   1142        '  top: 0px;' +
   1143        '  left: 0px;' +
   1144        '  width: 20px;' +
   1145        '  height: 20px;' +
   1146        '  transform: translateX(0px) translateY(0px);' +
   1147        '  transition: transform .5s;' +
   1148        '  background: #f00;' +
   1149        '  border: none;' +
   1150        '}';
   1151 
   1152    styles.innerHTML = cssText;
   1153  }
   1154 
   1155 
   1156  /**
   1157   * Adds the DOM fixtures used by all tests to the page and assigns them to
   1158   * global variables so they can be referenced within the tests.
   1159   */
   1160  function addFixtures() {
   1161    var fixtures = document.createElement('div');
   1162    fixtures.id = 'fixtures';
   1163 
   1164    fixtures.innerHTML =
   1165        '<div id="root">' +
   1166        '  <div id="grand-parent">' +
   1167        '    <div id="parent">' +
   1168        '      <div id="target1"></div>' +
   1169        '      <div id="target2"></div>' +
   1170        '      <div id="target3"></div>' +
   1171        '      <iframe id="target4"></iframe>' +
   1172        '    </div>' +
   1173        '  </div>' +
   1174        '</div>';
   1175 
   1176    document.body.appendChild(fixtures);
   1177 
   1178    rootEl = document.getElementById('root');
   1179    grandParentEl = document.getElementById('grand-parent');
   1180    parentEl = document.getElementById('parent');
   1181    targetEl1 = document.getElementById('target1');
   1182    targetEl2 = document.getElementById('target2');
   1183    targetEl3 = document.getElementById('target3');
   1184    targetEl4 = document.getElementById('target4');
   1185  }
   1186 
   1187 
   1188  /**
   1189   * Removes the common styles from the page.
   1190   */
   1191  function removeStyles() {
   1192    var styles = document.getElementById('styles');
   1193    styles.remove();
   1194  }
   1195 
   1196 
   1197  /**
   1198   * Removes the DOM fixtures from the page and resets the global references.
   1199   */
   1200  function removeFixtures() {
   1201    var fixtures = document.getElementById('fixtures');
   1202    fixtures.remove();
   1203 
   1204    rootEl = null;
   1205    grandParentEl = null;
   1206    parentEl = null;
   1207    targetEl1 = null;
   1208    targetEl2 = null;
   1209    targetEl3 = null;
   1210    targetEl4 = null;
   1211  }
   1212 
   1213  function onLoad() {
   1214    next();
   1215  }
   1216 
   1217  SimpleTest.waitForExplicitFinish();
   1218 </script>
   1219 </pre>
   1220 <div id="log">
   1221 </div>
   1222 </body>
   1223 </html>