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>