intersection-observer 0.5.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -69,12 +69,15 @@ describe('IntersectionObserver', function() {
69
69
  io = new IntersectionObserver(noop);
70
70
  expect(io.root).to.be(null);
71
71
 
72
+ io = new IntersectionObserver(noop, {root: document});
73
+ expect(io.root).to.be(document);
74
+
72
75
  io = new IntersectionObserver(noop, {root: rootEl});
73
76
  expect(io.root).to.be(rootEl);
74
77
  });
75
78
 
76
79
 
77
- it('throws when root is not an Element', function() {
80
+ it('throws when root is not a Document or Element', function() {
78
81
  expect(function() {
79
82
  io = new IntersectionObserver(noop, {root: 'foo'});
80
83
  }).to.throwException();
@@ -157,6 +160,21 @@ describe('IntersectionObserver', function() {
157
160
  }).to.throwException();
158
161
  });
159
162
 
163
+ it('fills in x and y in the resulting rects', function(done) {
164
+ io = new IntersectionObserver(function(records) {
165
+ expect(records.length).to.be(1);
166
+ var entry = records[0];
167
+ expect(entry.rootBounds.x).to.be(entry.rootBounds.left);
168
+ expect(entry.rootBounds.y).to.be(entry.rootBounds.top);
169
+ expect(entry.boundingClientRect.x).to.be(entry.boundingClientRect.left);
170
+ expect(entry.boundingClientRect.y).to.be(entry.boundingClientRect.top);
171
+ expect(entry.intersectionRect.x).to.be(entry.intersectionRect.left);
172
+ expect(entry.intersectionRect.y).to.be(entry.intersectionRect.top);
173
+ done();
174
+ }, {root: rootEl});
175
+ targetEl2.style.top = '-40px';
176
+ io.observe(targetEl1);
177
+ });
160
178
 
161
179
  it('triggers for all targets when observing begins', function(done) {
162
180
  io = new IntersectionObserver(function(records) {
@@ -724,6 +742,30 @@ describe('IntersectionObserver', function() {
724
742
 
725
743
  io.observe(targetEl1);
726
744
  });
745
+
746
+ it('handles roots in shadow DOM', function(done) {
747
+ var shadowRoot = grandParentEl.attachShadow({mode: 'open'});
748
+
749
+ shadowRoot.innerHTML =
750
+ '<style>' +
751
+ '#slot-parent {' +
752
+ ' position: relative;' +
753
+ ' width: 400px;' +
754
+ ' height: 200px;' +
755
+ '}' +
756
+ '</style>' +
757
+ '<div id="slot-parent"><slot></slot></div>';
758
+
759
+ var slotParent = shadowRoot.getElementById('slot-parent');
760
+
761
+ io = new IntersectionObserver(function(records) {
762
+ expect(records.length).to.be(1);
763
+ expect(records[0].intersectionRatio).to.be(1);
764
+ done();
765
+ }, {root: slotParent});
766
+
767
+ io.observe(targetEl1);
768
+ });
727
769
  }
728
770
 
729
771
 
@@ -917,6 +959,2213 @@ describe('IntersectionObserver', function() {
917
959
 
918
960
  });
919
961
 
962
+ describe('iframe', function() {
963
+ var iframe;
964
+ var iframeWin, iframeDoc;
965
+ var documentElement, body;
966
+ var iframeTargetEl1, iframeTargetEl2;
967
+ var bodyWidth;
968
+
969
+ beforeEach(function(done) {
970
+ iframe = document.createElement('iframe');
971
+ iframe.setAttribute('frameborder', '0');
972
+ iframe.setAttribute('scrolling', 'yes');
973
+ iframe.style.position = 'fixed';
974
+ iframe.style.top = '0px';
975
+ iframe.style.width = '100px';
976
+ iframe.style.height = '200px';
977
+ iframe.onerror = function() {
978
+ done(new Error('iframe initialization failed'));
979
+ };
980
+ iframe.onload = function() {
981
+ iframe.onload = null;
982
+ iframeWin = iframe.contentWindow;
983
+ iframeDoc = iframeWin.document;
984
+ iframeDoc.open();
985
+ iframeDoc.write('<!DOCTYPE html><html><body>');
986
+ iframeDoc.write('<style>');
987
+ iframeDoc.write('body {margin: 0}');
988
+ iframeDoc.write('.target {height: 200px; margin-bottom: 2px; background: blue;}');
989
+ iframeDoc.write('</style>');
990
+ iframeDoc.close();
991
+
992
+ // Ensure the documentElement and body are always sorted on top. See
993
+ // `sortRecords` for more info.
994
+ documentElement = iframeDoc.documentElement;
995
+ body = iframeDoc.body;
996
+ documentElement.id = 'A1';
997
+ body.id = 'A1';
998
+
999
+ function createTarget(id, bg) {
1000
+ var target = iframeDoc.createElement('div');
1001
+ target.id = id;
1002
+ target.className = 'target';
1003
+ target.style.background = bg;
1004
+ iframeDoc.body.appendChild(target);
1005
+ return target;
1006
+ }
1007
+ iframeTargetEl1 = createTarget('target1', 'blue');
1008
+ iframeTargetEl2 = createTarget('target2', 'green');
1009
+ bodyWidth = iframeDoc.body.clientWidth;
1010
+ done();
1011
+ };
1012
+ iframe.src = 'about:blank';
1013
+ rootEl.appendChild(iframe);
1014
+ });
1015
+
1016
+ afterEach(function() {
1017
+ rootEl.removeChild(iframe);
1018
+ });
1019
+
1020
+ function rect(r) {
1021
+ return {
1022
+ y: typeof r.y == 'number' ? r.y : r.top,
1023
+ x: typeof r.x == 'number' ? r.x : r.left,
1024
+ top: r.top,
1025
+ left: r.left,
1026
+ width: r.width != null ? r.width : r.right - r.left,
1027
+ height: r.height != null ? r.height : r.bottom - r.top,
1028
+ right: r.right != null ? r.right : r.left + r.width,
1029
+ bottom: r.bottom != null ? r.bottom : r.top + r.height
1030
+ };
1031
+ }
1032
+
1033
+ function getRootRect(doc) {
1034
+ var html = doc.documentElement;
1035
+ var body = doc.body;
1036
+ return rect({
1037
+ top: 0,
1038
+ left: 0,
1039
+ right: html.clientWidth || body.clientWidth,
1040
+ width: html.clientWidth || body.clientWidth,
1041
+ bottom: html.clientHeight || body.clientHeight,
1042
+ height: html.clientHeight || body.clientHeight
1043
+ });
1044
+ }
1045
+
1046
+ describe('same-origin iframe loaded in the mainframe', function() {
1047
+ it('iframe targets do not intersect with a top root element', function(done) {
1048
+ var io = new IntersectionObserver(function(unsortedRecords) {
1049
+ var records = sortRecords(unsortedRecords);
1050
+ expect(records.length).to.be(2);
1051
+ expect(records[0].isIntersecting).to.be(false);
1052
+ expect(records[1].isIntersecting).to.be(false);
1053
+ done();
1054
+ io.disconnect();
1055
+ }, {root: rootEl});
1056
+ io.observe(iframeTargetEl1);
1057
+ io.observe(iframeTargetEl2);
1058
+ });
1059
+
1060
+ it('triggers for all targets in top-level root', function(done) {
1061
+ var io = new IntersectionObserver(function(unsortedRecords) {
1062
+ var records = sortRecords(unsortedRecords);
1063
+ expect(records.length).to.be(2);
1064
+ expect(records[0].isIntersecting).to.be(true);
1065
+ expect(records[0].intersectionRatio).to.be(1);
1066
+ expect(records[1].isIntersecting).to.be(false);
1067
+ expect(records[1].intersectionRatio).to.be(0);
1068
+
1069
+ // The rootBounds is for the document's root.
1070
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1071
+
1072
+ done();
1073
+ io.disconnect();
1074
+ });
1075
+ io.observe(iframeTargetEl1);
1076
+ io.observe(iframeTargetEl2);
1077
+ });
1078
+
1079
+ it('triggers for all targets in iframe-level root', function(done) {
1080
+ var io = new IntersectionObserver(function(unsortedRecords) {
1081
+ var records = sortRecords(unsortedRecords);
1082
+ expect(records.length).to.be(2);
1083
+ expect(records[0].intersectionRatio).to.be(1);
1084
+ expect(records[1].intersectionRatio).to.be(1);
1085
+
1086
+ // The rootBounds is for the document's root.
1087
+ expect(rect(records[0].rootBounds)).
1088
+ to.eql(rect(iframeDoc.body.getBoundingClientRect()));
1089
+
1090
+ done();
1091
+ io.disconnect();
1092
+ }, {root: iframeDoc.body});
1093
+ io.observe(iframeTargetEl1);
1094
+ io.observe(iframeTargetEl2);
1095
+ });
1096
+
1097
+ it('calculates rects for a fully visible frame', function(done) {
1098
+ iframe.style.top = '0px';
1099
+ iframe.style.height = '300px';
1100
+ var io = new IntersectionObserver(function(unsortedRecords) {
1101
+ var records = sortRecords(unsortedRecords);
1102
+ expect(records.length).to.be(2);
1103
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1104
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1105
+
1106
+ // The target1 is fully visible.
1107
+ var clientRect1 = rect({
1108
+ top: 0,
1109
+ left: 0,
1110
+ width: bodyWidth,
1111
+ height: 200
1112
+ });
1113
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1114
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1115
+ expect(records[0].isIntersecting).to.be(true);
1116
+ expect(records[0].intersectionRatio).to.be(1);
1117
+
1118
+ // The target2 is partially visible.
1119
+ var clientRect2 = rect({
1120
+ top: 202,
1121
+ left: 0,
1122
+ width: bodyWidth,
1123
+ height: 200
1124
+ });
1125
+ var intersectRect2 = rect({
1126
+ top: 202,
1127
+ left: 0,
1128
+ width: bodyWidth,
1129
+ // The bottom is clipped off.
1130
+ bottom: 300
1131
+ });
1132
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1133
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1134
+ expect(records[1].isIntersecting).to.be(true);
1135
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
1136
+
1137
+ done();
1138
+ io.disconnect();
1139
+ });
1140
+ io.observe(iframeTargetEl1);
1141
+ io.observe(iframeTargetEl2);
1142
+ });
1143
+
1144
+ it('calculates rects for a fully visible and offset frame', function(done) {
1145
+ iframe.style.top = '10px';
1146
+ iframe.style.height = '300px';
1147
+ var io = new IntersectionObserver(function(unsortedRecords) {
1148
+ var records = sortRecords(unsortedRecords);
1149
+ expect(records.length).to.be(2);
1150
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1151
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1152
+
1153
+ // The target1 is fully visible.
1154
+ var clientRect1 = rect({
1155
+ top: 0,
1156
+ left: 0,
1157
+ width: bodyWidth,
1158
+ height: 200
1159
+ });
1160
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1161
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1162
+ expect(records[0].isIntersecting).to.be(true);
1163
+ expect(records[0].intersectionRatio).to.be(1);
1164
+
1165
+ // The target2 is partially visible.
1166
+ var clientRect2 = rect({
1167
+ top: 202,
1168
+ left: 0,
1169
+ width: bodyWidth,
1170
+ height: 200
1171
+ });
1172
+ var intersectRect2 = rect({
1173
+ top: 202,
1174
+ left: 0,
1175
+ width: bodyWidth,
1176
+ // The bottom is clipped off.
1177
+ bottom: 300
1178
+ });
1179
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1180
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1181
+ expect(records[1].isIntersecting).to.be(true);
1182
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
1183
+
1184
+ done();
1185
+ io.disconnect();
1186
+ });
1187
+ io.observe(iframeTargetEl1);
1188
+ io.observe(iframeTargetEl2);
1189
+ });
1190
+
1191
+ it('calculates rects for a clipped frame on top', function(done) {
1192
+ iframe.style.top = '-10px';
1193
+ iframe.style.height = '300px';
1194
+ var io = new IntersectionObserver(function(unsortedRecords) {
1195
+ var records = sortRecords(unsortedRecords);
1196
+ expect(records.length).to.be(2);
1197
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1198
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1199
+
1200
+ // The target1 is clipped at the top by the iframe's clipping.
1201
+ var clientRect1 = rect({
1202
+ top: 0,
1203
+ left: 0,
1204
+ width: bodyWidth,
1205
+ height: 200
1206
+ });
1207
+ var intersectRect1 = rect({
1208
+ left: 0,
1209
+ width: bodyWidth,
1210
+ // Top is clipped.
1211
+ top: 10,
1212
+ height: 200 - 10
1213
+ });
1214
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1215
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1216
+ expect(records[0].isIntersecting).to.be(true);
1217
+ expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1218
+
1219
+ // The target2 is partially visible.
1220
+ var clientRect2 = rect({
1221
+ top: 202,
1222
+ left: 0,
1223
+ width: bodyWidth,
1224
+ height: 200
1225
+ });
1226
+ var intersectRect2 = rect({
1227
+ top: 202,
1228
+ left: 0,
1229
+ width: bodyWidth,
1230
+ // The bottom is clipped off.
1231
+ bottom: 300
1232
+ });
1233
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1234
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1235
+ expect(records[1].isIntersecting).to.be(true);
1236
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.49
1237
+
1238
+ done();
1239
+ io.disconnect();
1240
+ });
1241
+ io.observe(iframeTargetEl1);
1242
+ io.observe(iframeTargetEl2);
1243
+ });
1244
+
1245
+ it('calculates rects for a clipped frame on bottom', function(done) {
1246
+ iframe.style.top = 'auto';
1247
+ iframe.style.bottom = '-10px';
1248
+ iframe.style.height = '300px';
1249
+ var io = new IntersectionObserver(function(unsortedRecords) {
1250
+ var records = sortRecords(unsortedRecords);
1251
+ expect(records.length).to.be(2);
1252
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1253
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1254
+
1255
+ // The target1 is clipped at the top by the iframe's clipping.
1256
+ var clientRect1 = rect({
1257
+ top: 0,
1258
+ left: 0,
1259
+ width: bodyWidth,
1260
+ height: 200
1261
+ });
1262
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1263
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1264
+ expect(records[0].isIntersecting).to.be(true);
1265
+ expect(records[0].intersectionRatio).to.be(1);
1266
+
1267
+ // The target2 is partially visible.
1268
+ var clientRect2 = rect({
1269
+ top: 202,
1270
+ left: 0,
1271
+ width: bodyWidth,
1272
+ height: 200
1273
+ });
1274
+ var intersectRect2 = rect({
1275
+ top: 202,
1276
+ left: 0,
1277
+ width: bodyWidth,
1278
+ // The bottom is clipped off.
1279
+ bottom: 300 - 10
1280
+ });
1281
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1282
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1283
+ expect(records[1].isIntersecting).to.be(true);
1284
+ expect(records[1].intersectionRatio).to.be.within(0.43, 0.45); // ~0.44
1285
+
1286
+ done();
1287
+ io.disconnect();
1288
+ });
1289
+ io.observe(iframeTargetEl1);
1290
+ io.observe(iframeTargetEl2);
1291
+ });
1292
+
1293
+ it('calculates rects for a fully visible frame and scrolled', function(done) {
1294
+ iframe.style.top = '0px';
1295
+ iframe.style.height = '300px';
1296
+ iframeWin.scrollTo(0, 10);
1297
+ var io = new IntersectionObserver(function(unsortedRecords) {
1298
+ var records = sortRecords(unsortedRecords);
1299
+ expect(records.length).to.be(2);
1300
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1301
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1302
+
1303
+ // The target1 is fully visible.
1304
+ var clientRect1 = rect({
1305
+ top: -10,
1306
+ left: 0,
1307
+ width: bodyWidth,
1308
+ height: 200
1309
+ });
1310
+ var intersectRect1 = rect({
1311
+ top: 0,
1312
+ left: 0,
1313
+ width: bodyWidth,
1314
+ // Height is only for the visible area.
1315
+ height: 200 - 10
1316
+ });
1317
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1318
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1319
+ expect(records[0].isIntersecting).to.be(true);
1320
+ expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1321
+
1322
+ // The target2 is partially visible.
1323
+ var clientRect2 = rect({
1324
+ top: 202 - 10,
1325
+ left: 0,
1326
+ width: bodyWidth,
1327
+ height: 200
1328
+ });
1329
+ var intersectRect2 = rect({
1330
+ top: 202 - 10,
1331
+ left: 0,
1332
+ width: bodyWidth,
1333
+ // The bottom is clipped off.
1334
+ bottom: 300
1335
+ });
1336
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1337
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1338
+ expect(records[1].isIntersecting).to.be(true);
1339
+ expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
1340
+
1341
+ done();
1342
+ io.disconnect();
1343
+ });
1344
+ io.observe(iframeTargetEl1);
1345
+ io.observe(iframeTargetEl2);
1346
+ });
1347
+
1348
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
1349
+ iframe.style.top = '-10px';
1350
+ iframe.style.height = '300px';
1351
+ iframeWin.scrollTo(0, 10);
1352
+ var io = new IntersectionObserver(function(unsortedRecords) {
1353
+ var records = sortRecords(unsortedRecords);
1354
+ expect(records.length).to.be(2);
1355
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1356
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1357
+
1358
+ // The target1 is clipped at the top by the iframe's clipping.
1359
+ var clientRect1 = rect({
1360
+ top: -10,
1361
+ left: 0,
1362
+ width: bodyWidth,
1363
+ height: 200
1364
+ });
1365
+ var intersectRect1 = rect({
1366
+ left: 0,
1367
+ width: bodyWidth,
1368
+ // Top is clipped.
1369
+ top: 10,
1370
+ // The height is less by both: offset and scroll.
1371
+ height: 200 - 10 - 10
1372
+ });
1373
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1374
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1375
+ expect(records[0].isIntersecting).to.be(true);
1376
+ expect(records[0].intersectionRatio).to.within(0.89, 0.91); // ~0.9
1377
+
1378
+ // The target2 is partially visible.
1379
+ var clientRect2 = rect({
1380
+ top: 202 - 10,
1381
+ left: 0,
1382
+ width: bodyWidth,
1383
+ height: 200
1384
+ });
1385
+ var intersectRect2 = rect({
1386
+ top: 202 - 10,
1387
+ left: 0,
1388
+ width: bodyWidth,
1389
+ // The bottom is clipped off.
1390
+ bottom: 300
1391
+ });
1392
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1393
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1394
+ expect(records[1].isIntersecting).to.be(true);
1395
+ expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
1396
+
1397
+ done();
1398
+ io.disconnect();
1399
+ });
1400
+ io.observe(iframeTargetEl1);
1401
+ io.observe(iframeTargetEl2);
1402
+ });
1403
+
1404
+ it('handles tracking iframe viewport', function(done) {
1405
+ iframe.style.height = '100px';
1406
+ iframe.style.top = '100px';
1407
+ iframeWin.scrollTo(0, 110);
1408
+ // {root:iframeDoc} means to track the iframe viewport.
1409
+ var io = new IntersectionObserver(
1410
+ function (records) {
1411
+ io.unobserve(iframeTargetEl1);
1412
+
1413
+ var intersectionRect = rect({
1414
+ top: 0, // if root=null, then this would be 100.
1415
+ left: 0,
1416
+ height: 90,
1417
+ width: bodyWidth
1418
+ });
1419
+ expect(records.length).to.be(1);
1420
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
1421
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1422
+ done();
1423
+ },
1424
+ { root: iframeDoc }
1425
+ );
1426
+
1427
+ io.observe(iframeTargetEl1);
1428
+ });
1429
+
1430
+ it('handles tracking iframe viewport with rootMargin', function(done) {
1431
+ iframe.style.height = '100px';
1432
+
1433
+ var io = new IntersectionObserver(
1434
+ function (records) {
1435
+ io.unobserve(iframeTargetEl1);
1436
+ var intersectionRect = rect({
1437
+ top: 0, // if root=null, then this would be 100.
1438
+ left: 0,
1439
+ height: 200,
1440
+ width: bodyWidth
1441
+ });
1442
+
1443
+ // rootMargin: 100% --> 3x width + 3x height.
1444
+ var expectedRootBounds = rect({
1445
+ top: -100,
1446
+ left: -bodyWidth,
1447
+ width: bodyWidth * 3,
1448
+ height: 100 * 3
1449
+ });
1450
+ expect(records.length).to.be(1);
1451
+ expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
1452
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1453
+ done();
1454
+ },
1455
+ { root: iframeDoc, rootMargin: '100%' }
1456
+ );
1457
+
1458
+ io.observe(iframeTargetEl1);
1459
+ });
1460
+
1461
+ // Current spec indicates that cross-document tracking yields
1462
+ // an essentially empty IntersectionObserverEntry.
1463
+ // See: https://github.com/w3c/IntersectionObserver/issues/87
1464
+ it('does not track cross-document elements', function(done) {
1465
+ var io = new IntersectionObserver(
1466
+ function (records) {
1467
+ io.unobserve(iframeTargetEl1)
1468
+
1469
+ expect(records.length).to.be(1);
1470
+ const zeroesRect = rect({
1471
+ top: 0,
1472
+ left: 0,
1473
+ width: 0,
1474
+ height: 0
1475
+ });
1476
+ expect(rect(records[0].rootBounds)).to.eql(zeroesRect);
1477
+ expect(rect(records[0].intersectionRect)).to.eql(zeroesRect);
1478
+ expect(records.isIntersecting).false;
1479
+ done();
1480
+ },
1481
+ { root: document }
1482
+ );
1483
+
1484
+ io.observe(iframeTargetEl1);
1485
+ });
1486
+
1487
+ it('handles style changes', function(done) {
1488
+ var spy = sinon.spy();
1489
+
1490
+ // When first element becomes invisible, the second element will show.
1491
+ // And in reverse: when the first element becomes visible again, the
1492
+ // second element will disappear.
1493
+ var io = new IntersectionObserver(spy);
1494
+ io.observe(iframeTargetEl1);
1495
+ io.observe(iframeTargetEl2);
1496
+
1497
+ runSequence([
1498
+ function(done) {
1499
+ setTimeout(function() {
1500
+ expect(spy.callCount).to.be(1);
1501
+ var records = sortRecords(spy.lastCall.args[0]);
1502
+ expect(records.length).to.be(2);
1503
+ expect(records[0].intersectionRatio).to.be(1);
1504
+ expect(records[0].isIntersecting).to.be(true);
1505
+ expect(records[1].intersectionRatio).to.be(0);
1506
+ expect(records[1].isIntersecting).to.be(false);
1507
+ done();
1508
+ }, ASYNC_TIMEOUT);
1509
+ },
1510
+ function(done) {
1511
+ iframeTargetEl1.style.display = 'none';
1512
+ setTimeout(function() {
1513
+ expect(spy.callCount).to.be(2);
1514
+ var records = sortRecords(spy.lastCall.args[0]);
1515
+ expect(records.length).to.be(2);
1516
+ expect(records[0].intersectionRatio).to.be(0);
1517
+ expect(records[0].isIntersecting).to.be(false);
1518
+ expect(records[1].intersectionRatio).to.be(1);
1519
+ expect(records[1].isIntersecting).to.be(true);
1520
+ done();
1521
+ }, ASYNC_TIMEOUT);
1522
+ },
1523
+ function(done) {
1524
+ iframeTargetEl1.style.display = '';
1525
+ setTimeout(function() {
1526
+ expect(spy.callCount).to.be(3);
1527
+ var records = sortRecords(spy.lastCall.args[0]);
1528
+ expect(records.length).to.be(2);
1529
+ expect(records[0].intersectionRatio).to.be(1);
1530
+ expect(records[0].isIntersecting).to.be(true);
1531
+ expect(records[1].intersectionRatio).to.be(0);
1532
+ expect(records[1].isIntersecting).to.be(false);
1533
+ done();
1534
+ }, ASYNC_TIMEOUT);
1535
+ },
1536
+ function(done) {
1537
+ io.disconnect();
1538
+ done();
1539
+ }
1540
+ ], done);
1541
+ });
1542
+
1543
+ it('handles scroll changes', function(done) {
1544
+ var spy = sinon.spy();
1545
+
1546
+ // Scrolling to the middle of the iframe shows the second box and
1547
+ // hides the first.
1548
+ var io = new IntersectionObserver(spy);
1549
+ io.observe(iframeTargetEl1);
1550
+ io.observe(iframeTargetEl2);
1551
+
1552
+ runSequence([
1553
+ function(done) {
1554
+ setTimeout(function() {
1555
+ expect(spy.callCount).to.be(1);
1556
+ var records = sortRecords(spy.lastCall.args[0]);
1557
+ expect(records.length).to.be(2);
1558
+ expect(records[0].intersectionRatio).to.be(1);
1559
+ expect(records[0].isIntersecting).to.be(true);
1560
+ expect(records[1].intersectionRatio).to.be(0);
1561
+ expect(records[1].isIntersecting).to.be(false);
1562
+ done();
1563
+ }, ASYNC_TIMEOUT);
1564
+ },
1565
+ function(done) {
1566
+ iframeWin.scrollTo(0, 202);
1567
+ setTimeout(function() {
1568
+ expect(spy.callCount).to.be(2);
1569
+ var records = sortRecords(spy.lastCall.args[0]);
1570
+ expect(records.length).to.be(2);
1571
+ expect(records[0].intersectionRatio).to.be(0);
1572
+ expect(records[0].isIntersecting).to.be(false);
1573
+ expect(records[1].intersectionRatio).to.be(1);
1574
+ expect(records[1].isIntersecting).to.be(true);
1575
+ done();
1576
+ }, ASYNC_TIMEOUT);
1577
+ },
1578
+ function(done) {
1579
+ iframeWin.scrollTo(0, 0);
1580
+ setTimeout(function() {
1581
+ expect(spy.callCount).to.be(3);
1582
+ var records = sortRecords(spy.lastCall.args[0]);
1583
+ expect(records.length).to.be(2);
1584
+ expect(records[0].intersectionRatio).to.be(1);
1585
+ expect(records[0].isIntersecting).to.be(true);
1586
+ expect(records[1].intersectionRatio).to.be(0);
1587
+ expect(records[1].isIntersecting).to.be(false);
1588
+ done();
1589
+ }, ASYNC_TIMEOUT);
1590
+ },
1591
+ function(done) {
1592
+ io.disconnect();
1593
+ done();
1594
+ }
1595
+ ], done);
1596
+ });
1597
+
1598
+ it('handles iframe changes', function(done) {
1599
+ var spy = sinon.spy();
1600
+
1601
+ // Iframe goes off screen and returns.
1602
+ var io = new IntersectionObserver(spy);
1603
+ io.observe(iframeTargetEl1);
1604
+ io.observe(iframeTargetEl2);
1605
+
1606
+ runSequence([
1607
+ function(done) {
1608
+ setTimeout(function() {
1609
+ expect(spy.callCount).to.be(1);
1610
+ var records = sortRecords(spy.lastCall.args[0]);
1611
+ expect(records.length).to.be(2);
1612
+ expect(records[0].intersectionRatio).to.be(1);
1613
+ expect(records[0].isIntersecting).to.be(true);
1614
+ expect(records[1].intersectionRatio).to.be(0);
1615
+ expect(records[1].isIntersecting).to.be(false);
1616
+ // Top-level bounds.
1617
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1618
+ expect(records[0].intersectionRect.height).to.be(200);
1619
+ done();
1620
+ }, ASYNC_TIMEOUT);
1621
+ },
1622
+ function(done) {
1623
+ // Completely off screen.
1624
+ iframe.style.top = '-202px';
1625
+ setTimeout(function() {
1626
+ expect(spy.callCount).to.be(2);
1627
+ var records = sortRecords(spy.lastCall.args[0]);
1628
+ expect(records.length).to.be(1);
1629
+ expect(records[0].intersectionRatio).to.be(0);
1630
+ expect(records[0].isIntersecting).to.be(false);
1631
+ // Top-level bounds.
1632
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1633
+ expect(records[0].intersectionRect.height).to.be(0);
1634
+ done();
1635
+ }, ASYNC_TIMEOUT);
1636
+ },
1637
+ function(done) {
1638
+ // Partially returns.
1639
+ iframe.style.top = '-100px';
1640
+ setTimeout(function() {
1641
+ expect(spy.callCount).to.be(3);
1642
+ var records = sortRecords(spy.lastCall.args[0]);
1643
+ expect(records.length).to.be(1);
1644
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
1645
+ expect(records[0].isIntersecting).to.be(true);
1646
+ // Top-level bounds.
1647
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1648
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
1649
+ done();
1650
+ }, ASYNC_TIMEOUT);
1651
+ },
1652
+ function(done) {
1653
+ io.disconnect();
1654
+ done();
1655
+ }
1656
+ ], done);
1657
+ });
1658
+
1659
+ it('continues to monitor until the last target unobserved', function(done) {
1660
+ var spy = sinon.spy();
1661
+
1662
+ // Iframe goes off screen and returns.
1663
+ var io = new IntersectionObserver(spy);
1664
+ io.observe(target1);
1665
+ io.observe(iframeTargetEl1);
1666
+ io.observe(iframeTargetEl2);
1667
+
1668
+ runSequence([
1669
+ function(done) {
1670
+ setTimeout(function() {
1671
+ expect(spy.callCount).to.be(1);
1672
+ expect(spy.lastCall.args[0].length).to.be(3);
1673
+
1674
+ // Unobserve one from the main context and one from iframe.
1675
+ io.unobserve(target1);
1676
+ io.unobserve(iframeTargetEl2);
1677
+
1678
+ done();
1679
+ }, ASYNC_TIMEOUT);
1680
+ },
1681
+ function(done) {
1682
+ // Completely off screen.
1683
+ iframe.style.top = '-202px';
1684
+ setTimeout(function() {
1685
+ expect(spy.callCount).to.be(2);
1686
+ expect(spy.lastCall.args[0].length).to.be(1);
1687
+
1688
+ io.unobserve(iframeTargetEl1);
1689
+
1690
+ done();
1691
+ }, ASYNC_TIMEOUT);
1692
+ },
1693
+ function(done) {
1694
+ // Partially returns.
1695
+ iframe.style.top = '-100px';
1696
+ setTimeout(function() {
1697
+ expect(spy.callCount).to.be(2);
1698
+ done();
1699
+ }, ASYNC_TIMEOUT);
1700
+ },
1701
+ function(done) {
1702
+ io.disconnect();
1703
+ done();
1704
+ }
1705
+ ], done);
1706
+ });
1707
+ });
1708
+
1709
+ describe('same-origin iframe loaded in an iframe', function() {
1710
+ var ASYNC_TIMEOUT = 300;
1711
+
1712
+ beforeEach(function(done) {
1713
+ /* Uncomment these lines to force polyfill inside the iframe.
1714
+ delete iframeWin.IntersectionObserver;
1715
+ delete iframeWin.IntersectionObserverEntry;
1716
+ */
1717
+
1718
+ // Install polyfill right into the iframe.
1719
+ if (!iframeWin.IntersectionObserver) {
1720
+ var script = iframeDoc.createElement('script');
1721
+ script.src = 'intersection-observer.js';
1722
+ script.onload = function() {
1723
+ done();
1724
+ };
1725
+ iframeDoc.body.appendChild(script);
1726
+ } else {
1727
+ done();
1728
+ }
1729
+ });
1730
+
1731
+ function computeRectIntersection(rect1, rect2) {
1732
+ var top = Math.max(rect1.top, rect2.top);
1733
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
1734
+ var left = Math.max(rect1.left, rect2.left);
1735
+ var right = Math.min(rect1.right, rect2.right);
1736
+ var width = right - left;
1737
+ var height = bottom - top;
1738
+
1739
+ return (width >= 0 && height >= 0) && {
1740
+ top: top,
1741
+ bottom: bottom,
1742
+ left: left,
1743
+ right: right,
1744
+ width: width,
1745
+ height: height
1746
+ } || {
1747
+ top: 0,
1748
+ bottom: 0,
1749
+ left: 0,
1750
+ right: 0,
1751
+ width: 0,
1752
+ height: 0
1753
+ };
1754
+ }
1755
+
1756
+ function checkRootBounds(records) {
1757
+ if (!supportsNativeIntersectionObserver(iframeWin)) {
1758
+ records.forEach(function(record) {
1759
+ expect(rect(record.rootBounds)).to.eql(getRootRect(document));
1760
+ });
1761
+ }
1762
+ }
1763
+
1764
+ function applyParentRect(parentRect) {
1765
+ iframe.style.top = parentRect.top + 'px';
1766
+ iframe.style.left = parentRect.left + 'px';
1767
+ iframe.style.height = parentRect.height + 'px';
1768
+ iframe.style.width = parentRect.width + 'px';
1769
+ }
1770
+
1771
+ function createObserver(callback, options, parentRect) {
1772
+ var io = new iframeWin.IntersectionObserver(callback, options);
1773
+ if (parentRect) {
1774
+ applyParentRect(parentRect);
1775
+ }
1776
+ return io;
1777
+ }
1778
+
1779
+ it('calculates rects for a fully visible frame', function(done) {
1780
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
1781
+ var io = createObserver(function(unsortedRecords) {
1782
+ var records = sortRecords(unsortedRecords);
1783
+ expect(records.length).to.be(3);
1784
+ checkRootBounds(records);
1785
+
1786
+ // The documentElement is partially visible.
1787
+ expect(rect(records[0].boundingClientRect))
1788
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1789
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1790
+ top: 0,
1791
+ left: 0,
1792
+ width: bodyWidth,
1793
+ height: 300
1794
+ }));
1795
+ expect(records[0].isIntersecting).to.be(true);
1796
+ // 300 / 404 == ~0.743
1797
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
1798
+
1799
+ // The document.body is partially visible.
1800
+ expect(rect(records[1].boundingClientRect))
1801
+ .to.eql(rect(body.getBoundingClientRect()));
1802
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1803
+ top: 0,
1804
+ left: 0,
1805
+ width: bodyWidth,
1806
+ height: 300
1807
+ }));
1808
+ expect(records[1].isIntersecting).to.be(true);
1809
+ // 300 / 402 == ~0.746
1810
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
1811
+
1812
+ // The target1 is fully visible.
1813
+ var clientRect1 = rect({
1814
+ top: 0,
1815
+ left: 0,
1816
+ width: bodyWidth,
1817
+ height: 200
1818
+ });
1819
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1820
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1821
+ expect(records[2].isIntersecting).to.be(true);
1822
+ expect(records[2].intersectionRatio).to.be(1);
1823
+
1824
+ done();
1825
+ io.disconnect();
1826
+ }, {}, parentRect);
1827
+ io.observe(documentElement);
1828
+ io.observe(body);
1829
+ io.observe(iframeTargetEl1);
1830
+ });
1831
+
1832
+ it('calculates rects for a fully visible and offset frame', function(done) {
1833
+ var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
1834
+ var io = createObserver(function(unsortedRecords) {
1835
+ var records = sortRecords(unsortedRecords);
1836
+ expect(records.length).to.be(3);
1837
+ checkRootBounds(records);
1838
+
1839
+ // The documentElement is partially visible.
1840
+ expect(rect(records[0].boundingClientRect))
1841
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1842
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1843
+ top: 0,
1844
+ left: 0,
1845
+ width: bodyWidth,
1846
+ height: 300
1847
+ }));
1848
+ expect(records[0].isIntersecting).to.be(true);
1849
+ // 300 / 404 == ~0.743
1850
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
1851
+
1852
+ // The document.body is partially visible.
1853
+ expect(rect(records[1].boundingClientRect))
1854
+ .to.eql(rect(body.getBoundingClientRect()));
1855
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1856
+ top: 0,
1857
+ left: 0,
1858
+ width: bodyWidth,
1859
+ height: 300
1860
+ }));
1861
+ expect(records[1].isIntersecting).to.be(true);
1862
+ // 300 / 402 == ~0.746
1863
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
1864
+
1865
+ // The target1 is fully visible.
1866
+ var clientRect1 = rect({
1867
+ top: 0,
1868
+ left: 0,
1869
+ width: bodyWidth,
1870
+ height: 200
1871
+ });
1872
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1873
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1874
+ expect(records[2].isIntersecting).to.be(true);
1875
+ expect(records[2].intersectionRatio).to.be(1);
1876
+
1877
+ done();
1878
+ io.disconnect();
1879
+ }, {}, parentRect);
1880
+ io.observe(documentElement);
1881
+ io.observe(body);
1882
+ io.observe(iframeTargetEl1);
1883
+ });
1884
+
1885
+ it('calculates rects for a clipped frame on top', function(done) {
1886
+ var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
1887
+ var io = createObserver(function(unsortedRecords) {
1888
+ var records = sortRecords(unsortedRecords);
1889
+ expect(records.length).to.be(3);
1890
+ checkRootBounds(records);
1891
+
1892
+ // The documentElement is partially visible.
1893
+ expect(rect(records[0].boundingClientRect))
1894
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1895
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1896
+ top: 10,
1897
+ left: 0,
1898
+ width: bodyWidth,
1899
+ height: 300 - 10
1900
+ }));
1901
+ expect(records[0].isIntersecting).to.be(true);
1902
+ // (300 - 10) / 404 == ~0.717
1903
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
1904
+
1905
+ // The document.body is partially visible.
1906
+ expect(rect(records[1].boundingClientRect))
1907
+ .to.eql(rect(body.getBoundingClientRect()));
1908
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1909
+ top: 10,
1910
+ left: 0,
1911
+ width: bodyWidth,
1912
+ height: 300 - 10
1913
+ }));
1914
+ expect(records[1].isIntersecting).to.be(true);
1915
+ // (300 - 10) / 402 == ~0.721
1916
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
1917
+
1918
+ // The target1 is clipped at the top by the iframe's clipping.
1919
+ var clientRect1 = rect({
1920
+ top: 0,
1921
+ left: 0,
1922
+ width: bodyWidth,
1923
+ height: 200
1924
+ });
1925
+ var intersectRect1 = rect({
1926
+ left: 0,
1927
+ width: bodyWidth,
1928
+ // Top is clipped.
1929
+ top: 10,
1930
+ height: 200 - 10
1931
+ });
1932
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1933
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
1934
+ expect(records[2].isIntersecting).to.be(true);
1935
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1936
+
1937
+ done();
1938
+ io.disconnect();
1939
+ }, {}, parentRect);
1940
+ io.observe(documentElement);
1941
+ io.observe(body);
1942
+ io.observe(iframeTargetEl1);
1943
+ });
1944
+
1945
+ it('calculates rects for a clipped frame on bottom', function(done) {
1946
+ var rootRect = getRootRect(document);
1947
+ var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
1948
+ var io = createObserver(function(unsortedRecords) {
1949
+ var records = sortRecords(unsortedRecords);
1950
+ expect(records.length).to.be(3);
1951
+ checkRootBounds(records);
1952
+
1953
+ // The documentElement is partially visible.
1954
+ expect(rect(records[0].boundingClientRect))
1955
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1956
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1957
+ top: 0,
1958
+ left: 0,
1959
+ width: bodyWidth,
1960
+ height: 300 - 10
1961
+ }));
1962
+ expect(records[0].isIntersecting).to.be(true);
1963
+ // (300 - 10) / 404 == ~0.717
1964
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
1965
+
1966
+ // The document.body is partially visible.
1967
+ expect(rect(records[1].boundingClientRect))
1968
+ .to.eql(rect(body.getBoundingClientRect()));
1969
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1970
+ top: 0,
1971
+ left: 0,
1972
+ width: bodyWidth,
1973
+ height: 300 - 10
1974
+ }));
1975
+ expect(records[1].isIntersecting).to.be(true);
1976
+ // (300 - 10) / 402 == ~0.721
1977
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
1978
+
1979
+ // The target1 is clipped at the top by the iframe's clipping.
1980
+ var clientRect1 = rect({
1981
+ top: 0,
1982
+ left: 0,
1983
+ width: bodyWidth,
1984
+ height: 200
1985
+ });
1986
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1987
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1988
+ expect(records[2].isIntersecting).to.be(true);
1989
+ expect(records[2].intersectionRatio).to.be(1);
1990
+
1991
+ done();
1992
+ io.disconnect();
1993
+ }, {}, parentRect);
1994
+ io.observe(documentElement);
1995
+ io.observe(body);
1996
+ io.observe(iframeTargetEl1);
1997
+ });
1998
+
1999
+ it('calculates rects for a fully visible and scrolled frame', function(done) {
2000
+ iframeWin.scrollTo(0, 10);
2001
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2002
+ var io = createObserver(function(unsortedRecords) {
2003
+ var records = sortRecords(unsortedRecords);
2004
+ expect(records.length).to.be(3);
2005
+ checkRootBounds(records);
2006
+
2007
+ // The documentElement is partially visible.
2008
+ expect(rect(records[0].boundingClientRect))
2009
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2010
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2011
+ top: 0,
2012
+ left: 0,
2013
+ width: bodyWidth,
2014
+ height: 300
2015
+ }));
2016
+ expect(records[0].isIntersecting).to.be(true);
2017
+ // 300 / 404 == ~0.743
2018
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2019
+
2020
+ // The document.body is partially visible.
2021
+ expect(rect(records[1].boundingClientRect))
2022
+ .to.eql(rect(body.getBoundingClientRect()));
2023
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2024
+ top: 0,
2025
+ left: 0,
2026
+ width: bodyWidth,
2027
+ height: 300
2028
+ }));
2029
+ expect(records[1].isIntersecting).to.be(true);
2030
+ // 300 / 402 == ~0.746
2031
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2032
+
2033
+ // The target1 is fully visible.
2034
+ var clientRect1 = rect({
2035
+ top: -10,
2036
+ left: 0,
2037
+ width: bodyWidth,
2038
+ height: 200
2039
+ });
2040
+ var intersectRect1 = rect({
2041
+ top: 0,
2042
+ left: 0,
2043
+ width: bodyWidth,
2044
+ // Height is only for the visible area.
2045
+ height: 200 - 10
2046
+ });
2047
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2048
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2049
+ expect(records[2].isIntersecting).to.be(true);
2050
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
2051
+
2052
+ done();
2053
+ io.disconnect();
2054
+ }, {}, parentRect);
2055
+ io.observe(documentElement);
2056
+ io.observe(body);
2057
+ io.observe(iframeTargetEl1);
2058
+ });
2059
+
2060
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
2061
+ iframeWin.scrollTo(0, 10);
2062
+ var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
2063
+ var io = createObserver(function(unsortedRecords) {
2064
+ var records = sortRecords(unsortedRecords);
2065
+ expect(records.length).to.be(4);
2066
+ checkRootBounds(records);
2067
+
2068
+ // The documentElement is partially visible.
2069
+ expect(rect(records[0].boundingClientRect))
2070
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2071
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2072
+ top: 10,
2073
+ left: 0,
2074
+ width: bodyWidth,
2075
+ height: 300 - 10
2076
+ }));
2077
+ expect(records[0].isIntersecting).to.be(true);
2078
+ // (300 - 10) / 404 == ~0.717
2079
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2080
+
2081
+ // The document.body is partially visible.
2082
+ expect(rect(records[1].boundingClientRect))
2083
+ .to.eql(rect(body.getBoundingClientRect()));
2084
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2085
+ top: 10,
2086
+ left: 0,
2087
+ width: bodyWidth,
2088
+ height: 300 - 10
2089
+ }));
2090
+ expect(records[1].isIntersecting).to.be(true);
2091
+ // (300 - 10) / 402 == ~0.721
2092
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2093
+
2094
+ // The target1 is clipped at the top by the iframe's clipping.
2095
+ var clientRect1 = rect({
2096
+ top: -10,
2097
+ left: 0,
2098
+ width: bodyWidth,
2099
+ height: 200
2100
+ });
2101
+ var intersectRect1 = rect({
2102
+ left: 0,
2103
+ width: bodyWidth,
2104
+ // Top is clipped.
2105
+ top: 10,
2106
+ // The height is less by both: offset and scroll.
2107
+ height: 200 - 10 - 10
2108
+ });
2109
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2110
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2111
+ expect(records[2].isIntersecting).to.be(true);
2112
+ expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
2113
+
2114
+ // The target2 is partially visible.
2115
+ var clientRect2 = rect({
2116
+ top: 202 - 10,
2117
+ left: 0,
2118
+ width: bodyWidth,
2119
+ height: 200
2120
+ });
2121
+ var intersectRect2 = rect({
2122
+ top: 202 - 10,
2123
+ left: 0,
2124
+ width: bodyWidth,
2125
+ // The bottom is clipped off.
2126
+ bottom: 300
2127
+ });
2128
+ expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
2129
+ expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
2130
+ expect(records[3].isIntersecting).to.be(true);
2131
+ expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
2132
+
2133
+ done();
2134
+ io.disconnect();
2135
+ }, {}, parentRect);
2136
+ io.observe(documentElement);
2137
+ io.observe(body);
2138
+ io.observe(iframeTargetEl1);
2139
+ io.observe(iframeTargetEl2);
2140
+ });
2141
+
2142
+ it('calculates rects for a fully clipped frame', function(done) {
2143
+ var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
2144
+ var io = createObserver(function(unsortedRecords) {
2145
+ var records = sortRecords(unsortedRecords);
2146
+ expect(records.length).to.be(3);
2147
+ checkRootBounds(records);
2148
+
2149
+ var emptyRect = rect({
2150
+ top: 0,
2151
+ left: 0,
2152
+ width: 0,
2153
+ height: 0
2154
+ });
2155
+
2156
+ // The documentElement is completely invisible.
2157
+ expect(rect(records[0].boundingClientRect))
2158
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2159
+ expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
2160
+ expect(records[0].isIntersecting).to.be(false);
2161
+ expect(records[0].intersectionRatio).to.be(0);
2162
+
2163
+ // The document.body is completely invisible.
2164
+ expect(rect(records[1].boundingClientRect))
2165
+ .to.eql(rect(body.getBoundingClientRect()));
2166
+ expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
2167
+ expect(records[1].isIntersecting).to.be(false);
2168
+ expect(records[1].intersectionRatio).to.be(0);
2169
+
2170
+ // The target1 is completely invisible.
2171
+ var clientRect1 = rect({
2172
+ top: 0,
2173
+ left: 0,
2174
+ width: bodyWidth,
2175
+ height: 200
2176
+ });
2177
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2178
+ expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
2179
+ expect(records[2].isIntersecting).to.be(false);
2180
+ expect(records[2].intersectionRatio).to.be(0);
2181
+
2182
+ done();
2183
+ io.disconnect();
2184
+ }, {}, parentRect);
2185
+ io.observe(documentElement);
2186
+ io.observe(body);
2187
+ io.observe(iframeTargetEl1);
2188
+ });
2189
+
2190
+ it('handles style changes', function(done) {
2191
+ var spy = sinon.spy();
2192
+
2193
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2194
+
2195
+ // When first element becomes invisible, the second element will show.
2196
+ // And in reverse: when the first element becomes visible again, the
2197
+ // second element will disappear.
2198
+ var io = createObserver(spy, {}, parentRect);
2199
+ io.observe(iframeTargetEl1);
2200
+ io.observe(iframeTargetEl2);
2201
+
2202
+ runSequence([
2203
+ function(done) {
2204
+ setTimeout(function() {
2205
+ expect(spy.callCount).to.be(1);
2206
+ var records = sortRecords(spy.lastCall.args[0]);
2207
+ expect(records.length).to.be(2);
2208
+ expect(records[0].intersectionRatio).to.be(1);
2209
+ expect(records[0].isIntersecting).to.be(true);
2210
+ expect(records[1].intersectionRatio).to.be(0);
2211
+ expect(records[1].isIntersecting).to.be(false);
2212
+ done();
2213
+ }, ASYNC_TIMEOUT);
2214
+ },
2215
+ function(done) {
2216
+ iframeTargetEl1.style.display = 'none';
2217
+ setTimeout(function() {
2218
+ expect(spy.callCount).to.be(2);
2219
+ var records = sortRecords(spy.lastCall.args[0]);
2220
+ expect(records.length).to.be(2);
2221
+ expect(records[0].intersectionRatio).to.be(0);
2222
+ expect(records[0].isIntersecting).to.be(false);
2223
+ expect(records[1].intersectionRatio).to.be(1);
2224
+ expect(records[1].isIntersecting).to.be(true);
2225
+ done();
2226
+ }, ASYNC_TIMEOUT);
2227
+ },
2228
+ function(done) {
2229
+ iframeTargetEl1.style.display = '';
2230
+ setTimeout(function() {
2231
+ expect(spy.callCount).to.be(3);
2232
+ var records = sortRecords(spy.lastCall.args[0]);
2233
+ expect(records.length).to.be(2);
2234
+ expect(records[0].intersectionRatio).to.be(1);
2235
+ expect(records[0].isIntersecting).to.be(true);
2236
+ expect(records[1].intersectionRatio).to.be(0);
2237
+ expect(records[1].isIntersecting).to.be(false);
2238
+ done();
2239
+ }, ASYNC_TIMEOUT);
2240
+ },
2241
+ function(done) {
2242
+ io.disconnect();
2243
+ done();
2244
+ }
2245
+ ], done);
2246
+ });
2247
+
2248
+ it('handles scroll changes', function(done) {
2249
+ var spy = sinon.spy();
2250
+
2251
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2252
+
2253
+ // Scrolling to the middle of the iframe shows the second box and
2254
+ // hides the first.
2255
+ var io = createObserver(spy, {}, parentRect);
2256
+ io.observe(iframeTargetEl1);
2257
+ io.observe(iframeTargetEl2);
2258
+
2259
+ runSequence([
2260
+ function(done) {
2261
+ setTimeout(function() {
2262
+ expect(spy.callCount).to.be(1);
2263
+ var records = sortRecords(spy.lastCall.args[0]);
2264
+ expect(records.length).to.be(2);
2265
+ expect(records[0].intersectionRatio).to.be(1);
2266
+ expect(records[0].isIntersecting).to.be(true);
2267
+ expect(records[1].intersectionRatio).to.be(0);
2268
+ expect(records[1].isIntersecting).to.be(false);
2269
+ done();
2270
+ }, ASYNC_TIMEOUT);
2271
+ },
2272
+ function(done) {
2273
+ iframeWin.scrollTo(0, 202);
2274
+ setTimeout(function() {
2275
+ expect(spy.callCount).to.be(2);
2276
+ var records = sortRecords(spy.lastCall.args[0]);
2277
+ expect(records.length).to.be(2);
2278
+ expect(records[0].intersectionRatio).to.be(0);
2279
+ expect(records[0].isIntersecting).to.be(false);
2280
+ expect(records[1].intersectionRatio).to.be(1);
2281
+ expect(records[1].isIntersecting).to.be(true);
2282
+ done();
2283
+ }, ASYNC_TIMEOUT);
2284
+ },
2285
+ function(done) {
2286
+ iframeWin.scrollTo(0, 0);
2287
+ setTimeout(function() {
2288
+ expect(spy.callCount).to.be(3);
2289
+ var records = sortRecords(spy.lastCall.args[0]);
2290
+ expect(records.length).to.be(2);
2291
+ expect(records[0].intersectionRatio).to.be(1);
2292
+ expect(records[0].isIntersecting).to.be(true);
2293
+ expect(records[1].intersectionRatio).to.be(0);
2294
+ expect(records[1].isIntersecting).to.be(false);
2295
+ done();
2296
+ }, ASYNC_TIMEOUT);
2297
+ },
2298
+ function(done) {
2299
+ io.disconnect();
2300
+ done();
2301
+ }
2302
+ ], done);
2303
+ });
2304
+
2305
+ it('handles parent rect changes', function(done) {
2306
+ var spy = sinon.spy();
2307
+
2308
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2309
+
2310
+ // Iframe goes off screen and returns.
2311
+ var io = createObserver(spy, {}, parentRect);
2312
+ io.observe(iframeTargetEl1);
2313
+ io.observe(iframeTargetEl2);
2314
+
2315
+ runSequence([
2316
+ function(done) {
2317
+ setTimeout(function() {
2318
+ expect(spy.callCount).to.be(1);
2319
+ var records = sortRecords(spy.lastCall.args[0]);
2320
+ expect(records.length).to.be(2);
2321
+ checkRootBounds(records);
2322
+ expect(records[0].intersectionRatio).to.be(1);
2323
+ expect(records[0].isIntersecting).to.be(true);
2324
+ expect(records[1].intersectionRatio).to.be(0);
2325
+ expect(records[1].isIntersecting).to.be(false);
2326
+ // Top-level bounds.
2327
+ expect(records[0].intersectionRect.height).to.be(200);
2328
+ done();
2329
+ }, ASYNC_TIMEOUT);
2330
+ },
2331
+ function(done) {
2332
+ // Completely off screen.
2333
+ applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
2334
+ setTimeout(function() {
2335
+ expect(spy.callCount).to.be(2);
2336
+ var records = sortRecords(spy.lastCall.args[0]);
2337
+ expect(records.length).to.be(1);
2338
+ checkRootBounds(records);
2339
+ expect(records[0].intersectionRatio).to.be(0);
2340
+ expect(records[0].isIntersecting).to.be(false);
2341
+ // Top-level bounds.
2342
+ expect(records[0].intersectionRect.height).to.be(0);
2343
+ done();
2344
+ }, ASYNC_TIMEOUT);
2345
+ },
2346
+ function(done) {
2347
+ // Partially returns.
2348
+ applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
2349
+ setTimeout(function() {
2350
+ expect(spy.callCount).to.be(3);
2351
+ var records = sortRecords(spy.lastCall.args[0]);
2352
+ expect(records.length).to.be(1);
2353
+ checkRootBounds(records);
2354
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
2355
+ expect(records[0].isIntersecting).to.be(true);
2356
+ // Top-level bounds.
2357
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
2358
+ done();
2359
+ }, ASYNC_TIMEOUT);
2360
+ },
2361
+ function(done) {
2362
+ io.disconnect();
2363
+ done();
2364
+ }
2365
+ ], done);
2366
+ });
2367
+ });
2368
+
2369
+ describe('cross-origin iframe', function() {
2370
+ var ASYNC_TIMEOUT = 300;
2371
+ var crossOriginUpdater;
2372
+
2373
+ beforeEach(function(done) {
2374
+ Object.defineProperty(iframeWin, 'frameElement', {value: null});
2375
+
2376
+ /* Uncomment these lines to force polyfill inside the iframe.
2377
+ delete iframeWin.IntersectionObserver;
2378
+ delete iframeWin.IntersectionObserverEntry;
2379
+ */
2380
+
2381
+ // Install polyfill right into the iframe.
2382
+ if (!iframeWin.IntersectionObserver) {
2383
+ var script = iframeDoc.createElement('script');
2384
+ script.src = 'intersection-observer.js';
2385
+ script.onload = function() {
2386
+ if (iframeWin.IntersectionObserver._setupCrossOriginUpdater) {
2387
+ crossOriginUpdater = iframeWin.IntersectionObserver._setupCrossOriginUpdater();
2388
+ }
2389
+ done();
2390
+ };
2391
+ iframeDoc.body.appendChild(script);
2392
+ } else {
2393
+ done();
2394
+ }
2395
+ });
2396
+
2397
+ afterEach(function() {
2398
+ if (IntersectionObserver._resetCrossOriginUpdater) {
2399
+ IntersectionObserver._resetCrossOriginUpdater();
2400
+ }
2401
+ });
2402
+
2403
+ function computeRectIntersection(rect1, rect2) {
2404
+ var top = Math.max(rect1.top, rect2.top);
2405
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
2406
+ var left = Math.max(rect1.left, rect2.left);
2407
+ var right = Math.min(rect1.right, rect2.right);
2408
+ var width = right - left;
2409
+ var height = bottom - top;
2410
+
2411
+ return (width >= 0 && height >= 0) && {
2412
+ top: top,
2413
+ bottom: bottom,
2414
+ left: left,
2415
+ right: right,
2416
+ width: width,
2417
+ height: height
2418
+ } || {
2419
+ top: 0,
2420
+ bottom: 0,
2421
+ left: 0,
2422
+ right: 0,
2423
+ width: 0,
2424
+ height: 0
2425
+ };
2426
+ }
2427
+
2428
+ function checkRootBoundsAreNull(records) {
2429
+ if (!supportsNativeIntersectionObserver(iframeWin)) {
2430
+ records.forEach(function(record) {
2431
+ expect(record.rootBounds).to.be(null);
2432
+ });
2433
+ }
2434
+ }
2435
+
2436
+ function applyParentRect(parentRect) {
2437
+ if (crossOriginUpdater) {
2438
+ var parentIntersectionRect = computeRectIntersection(
2439
+ parentRect, getRootRect(document));
2440
+ crossOriginUpdater(parentRect, parentIntersectionRect);
2441
+ } else {
2442
+ iframe.style.top = parentRect.top + 'px';
2443
+ iframe.style.left = parentRect.left + 'px';
2444
+ iframe.style.height = parentRect.height + 'px';
2445
+ iframe.style.width = parentRect.width + 'px';
2446
+ }
2447
+ }
2448
+
2449
+ function createObserver(callback, options, parentRect) {
2450
+ var io = new iframeWin.IntersectionObserver(callback, options);
2451
+ if (parentRect) {
2452
+ applyParentRect(parentRect);
2453
+ }
2454
+ return io;
2455
+ }
2456
+
2457
+ it('calculates rects for a fully visible frame', function(done) {
2458
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2459
+ var io = createObserver(function(unsortedRecords) {
2460
+ var records = sortRecords(unsortedRecords);
2461
+ expect(records.length).to.be(3);
2462
+ checkRootBoundsAreNull(records);
2463
+
2464
+ // The documentElement is partially visible.
2465
+ expect(rect(records[0].boundingClientRect))
2466
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2467
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2468
+ top: 0,
2469
+ left: 0,
2470
+ width: bodyWidth,
2471
+ height: 300
2472
+ }));
2473
+ expect(records[0].isIntersecting).to.be(true);
2474
+ // 300 / 404 == ~0.743
2475
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2476
+
2477
+ // The document.body is partially visible.
2478
+ expect(rect(records[1].boundingClientRect))
2479
+ .to.eql(rect(body.getBoundingClientRect()));
2480
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2481
+ top: 0,
2482
+ left: 0,
2483
+ width: bodyWidth,
2484
+ height: 300
2485
+ }));
2486
+ expect(records[1].isIntersecting).to.be(true);
2487
+ // 300 / 402 == ~0.746
2488
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2489
+
2490
+ // The target1 is fully visible.
2491
+ var clientRect1 = rect({
2492
+ top: 0,
2493
+ left: 0,
2494
+ width: bodyWidth,
2495
+ height: 200
2496
+ });
2497
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2498
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2499
+ expect(records[2].isIntersecting).to.be(true);
2500
+ expect(records[2].intersectionRatio).to.be(1);
2501
+
2502
+ done();
2503
+ io.disconnect();
2504
+ }, {}, parentRect);
2505
+ io.observe(documentElement);
2506
+ io.observe(body);
2507
+ io.observe(iframeTargetEl1);
2508
+ });
2509
+
2510
+ it('calculates rects for a fully visible and offset frame', function(done) {
2511
+ var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
2512
+ var io = createObserver(function(unsortedRecords) {
2513
+ var records = sortRecords(unsortedRecords);
2514
+ expect(records.length).to.be(3);
2515
+ checkRootBoundsAreNull(records);
2516
+
2517
+ // The documentElement is partially visible.
2518
+ expect(rect(records[0].boundingClientRect))
2519
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2520
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2521
+ top: 0,
2522
+ left: 0,
2523
+ width: bodyWidth,
2524
+ height: 300
2525
+ }));
2526
+ expect(records[0].isIntersecting).to.be(true);
2527
+ // 300 / 404 == ~0.743
2528
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2529
+
2530
+ // The document.body is partially visible.
2531
+ expect(rect(records[1].boundingClientRect))
2532
+ .to.eql(rect(body.getBoundingClientRect()));
2533
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2534
+ top: 0,
2535
+ left: 0,
2536
+ width: bodyWidth,
2537
+ height: 300
2538
+ }));
2539
+ expect(records[1].isIntersecting).to.be(true);
2540
+ // 300 / 402 == ~0.746
2541
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2542
+
2543
+ // The target1 is fully visible.
2544
+ var clientRect1 = rect({
2545
+ top: 0,
2546
+ left: 0,
2547
+ width: bodyWidth,
2548
+ height: 200
2549
+ });
2550
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2551
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2552
+ expect(records[2].isIntersecting).to.be(true);
2553
+ expect(records[2].intersectionRatio).to.be(1);
2554
+
2555
+ done();
2556
+ io.disconnect();
2557
+ }, {}, parentRect);
2558
+ io.observe(documentElement);
2559
+ io.observe(body);
2560
+ io.observe(iframeTargetEl1);
2561
+ });
2562
+
2563
+ it('calculates rects for a clipped frame on top', function(done) {
2564
+ var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
2565
+ var io = createObserver(function(unsortedRecords) {
2566
+ var records = sortRecords(unsortedRecords);
2567
+ expect(records.length).to.be(3);
2568
+ checkRootBoundsAreNull(records);
2569
+
2570
+ // The documentElement is partially visible.
2571
+ expect(rect(records[0].boundingClientRect))
2572
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2573
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2574
+ top: 10,
2575
+ left: 0,
2576
+ width: bodyWidth,
2577
+ height: 300 - 10
2578
+ }));
2579
+ expect(records[0].isIntersecting).to.be(true);
2580
+ // (300 - 10) / 404 == ~0.717
2581
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2582
+
2583
+ // The document.body is partially visible.
2584
+ expect(rect(records[1].boundingClientRect))
2585
+ .to.eql(rect(body.getBoundingClientRect()));
2586
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2587
+ top: 10,
2588
+ left: 0,
2589
+ width: bodyWidth,
2590
+ height: 300 - 10
2591
+ }));
2592
+ expect(records[1].isIntersecting).to.be(true);
2593
+ // (300 - 10) / 402 == ~0.721
2594
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2595
+
2596
+ // The target1 is clipped at the top by the iframe's clipping.
2597
+ var clientRect1 = rect({
2598
+ top: 0,
2599
+ left: 0,
2600
+ width: bodyWidth,
2601
+ height: 200
2602
+ });
2603
+ var intersectRect1 = rect({
2604
+ left: 0,
2605
+ width: bodyWidth,
2606
+ // Top is clipped.
2607
+ top: 10,
2608
+ height: 200 - 10
2609
+ });
2610
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2611
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2612
+ expect(records[2].isIntersecting).to.be(true);
2613
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
2614
+
2615
+ done();
2616
+ io.disconnect();
2617
+ }, {}, parentRect);
2618
+ io.observe(documentElement);
2619
+ io.observe(body);
2620
+ io.observe(iframeTargetEl1);
2621
+ });
2622
+
2623
+ it('calculates rects for a clipped frame on bottom', function(done) {
2624
+ var rootRect = getRootRect(document);
2625
+ var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
2626
+ var io = createObserver(function(unsortedRecords) {
2627
+ var records = sortRecords(unsortedRecords);
2628
+ expect(records.length).to.be(3);
2629
+ checkRootBoundsAreNull(records);
2630
+
2631
+ // The documentElement is partially visible.
2632
+ expect(rect(records[0].boundingClientRect))
2633
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2634
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2635
+ top: 0,
2636
+ left: 0,
2637
+ width: bodyWidth,
2638
+ height: 300 - 10
2639
+ }));
2640
+ expect(records[0].isIntersecting).to.be(true);
2641
+ // (300 - 10) / 404 == ~0.717
2642
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2643
+
2644
+ // The document.body is partially visible.
2645
+ expect(rect(records[1].boundingClientRect))
2646
+ .to.eql(rect(body.getBoundingClientRect()));
2647
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2648
+ top: 0,
2649
+ left: 0,
2650
+ width: bodyWidth,
2651
+ height: 300 - 10
2652
+ }));
2653
+ expect(records[1].isIntersecting).to.be(true);
2654
+ // (300 - 10) / 402 == ~0.721
2655
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2656
+
2657
+ // The target1 is clipped at the top by the iframe's clipping.
2658
+ var clientRect1 = rect({
2659
+ top: 0,
2660
+ left: 0,
2661
+ width: bodyWidth,
2662
+ height: 200
2663
+ });
2664
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2665
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2666
+ expect(records[2].isIntersecting).to.be(true);
2667
+ expect(records[2].intersectionRatio).to.be(1);
2668
+
2669
+ done();
2670
+ io.disconnect();
2671
+ }, {}, parentRect);
2672
+ io.observe(documentElement);
2673
+ io.observe(body);
2674
+ io.observe(iframeTargetEl1);
2675
+ });
2676
+
2677
+ it('calculates rects for a fully visible and scrolled frame', function(done) {
2678
+ iframeWin.scrollTo(0, 10);
2679
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2680
+ var io = createObserver(function(unsortedRecords) {
2681
+ var records = sortRecords(unsortedRecords);
2682
+ expect(records.length).to.be(3);
2683
+ checkRootBoundsAreNull(records);
2684
+
2685
+ // The documentElement is partially visible.
2686
+ expect(rect(records[0].boundingClientRect))
2687
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2688
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2689
+ top: 0,
2690
+ left: 0,
2691
+ width: bodyWidth,
2692
+ height: 300
2693
+ }));
2694
+ expect(records[0].isIntersecting).to.be(true);
2695
+ // 300 / 404 == ~0.743
2696
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2697
+
2698
+ // The document.body is partially visible.
2699
+ expect(rect(records[1].boundingClientRect))
2700
+ .to.eql(rect(body.getBoundingClientRect()));
2701
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2702
+ top: 0,
2703
+ left: 0,
2704
+ width: bodyWidth,
2705
+ height: 300
2706
+ }));
2707
+ expect(records[1].isIntersecting).to.be(true);
2708
+ // 300 / 402 == ~0.746
2709
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2710
+
2711
+ // The target1 is fully visible.
2712
+ var clientRect1 = rect({
2713
+ top: -10,
2714
+ left: 0,
2715
+ width: bodyWidth,
2716
+ height: 200
2717
+ });
2718
+ var intersectRect1 = rect({
2719
+ top: 0,
2720
+ left: 0,
2721
+ width: bodyWidth,
2722
+ // Height is only for the visible area.
2723
+ height: 200 - 10
2724
+ });
2725
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2726
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2727
+ expect(records[2].isIntersecting).to.be(true);
2728
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
2729
+
2730
+ done();
2731
+ io.disconnect();
2732
+ }, {}, parentRect);
2733
+ io.observe(documentElement);
2734
+ io.observe(body);
2735
+ io.observe(iframeTargetEl1);
2736
+ });
2737
+
2738
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
2739
+ iframeWin.scrollTo(0, 10);
2740
+ var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
2741
+ var io = createObserver(function(unsortedRecords) {
2742
+ var records = sortRecords(unsortedRecords);
2743
+ expect(records.length).to.be(4);
2744
+ checkRootBoundsAreNull(records);
2745
+
2746
+ // The documentElement is partially visible.
2747
+ expect(rect(records[0].boundingClientRect))
2748
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2749
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2750
+ top: 10,
2751
+ left: 0,
2752
+ width: bodyWidth,
2753
+ height: 300 - 10
2754
+ }));
2755
+ expect(records[0].isIntersecting).to.be(true);
2756
+ // (300 - 10) / 404 == ~0.717
2757
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2758
+
2759
+ // The document.body is partially visible.
2760
+ expect(rect(records[1].boundingClientRect))
2761
+ .to.eql(rect(body.getBoundingClientRect()));
2762
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2763
+ top: 10,
2764
+ left: 0,
2765
+ width: bodyWidth,
2766
+ height: 300 - 10
2767
+ }));
2768
+ expect(records[1].isIntersecting).to.be(true);
2769
+ // (300 - 10) / 402 == ~0.721
2770
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2771
+
2772
+ // The target1 is clipped at the top by the iframe's clipping.
2773
+ var clientRect1 = rect({
2774
+ top: -10,
2775
+ left: 0,
2776
+ width: bodyWidth,
2777
+ height: 200
2778
+ });
2779
+ var intersectRect1 = rect({
2780
+ left: 0,
2781
+ width: bodyWidth,
2782
+ // Top is clipped.
2783
+ top: 10,
2784
+ // The height is less by both: offset and scroll.
2785
+ height: 200 - 10 - 10
2786
+ });
2787
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2788
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2789
+ expect(records[2].isIntersecting).to.be(true);
2790
+ expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
2791
+
2792
+ // The target2 is partially visible.
2793
+ var clientRect2 = rect({
2794
+ top: 202 - 10,
2795
+ left: 0,
2796
+ width: bodyWidth,
2797
+ height: 200
2798
+ });
2799
+ var intersectRect2 = rect({
2800
+ top: 202 - 10,
2801
+ left: 0,
2802
+ width: bodyWidth,
2803
+ // The bottom is clipped off.
2804
+ bottom: 300
2805
+ });
2806
+ expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
2807
+ expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
2808
+ expect(records[3].isIntersecting).to.be(true);
2809
+ expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
2810
+
2811
+ done();
2812
+ io.disconnect();
2813
+ }, {}, parentRect);
2814
+ io.observe(documentElement);
2815
+ io.observe(body);
2816
+ io.observe(iframeTargetEl1);
2817
+ io.observe(iframeTargetEl2);
2818
+ });
2819
+
2820
+ it('calculates rects for a fully clipped frame', function(done) {
2821
+ var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
2822
+ var io = createObserver(function(unsortedRecords) {
2823
+ var records = sortRecords(unsortedRecords);
2824
+ expect(records.length).to.be(3);
2825
+ checkRootBoundsAreNull(records);
2826
+
2827
+ var emptyRect = rect({
2828
+ top: 0,
2829
+ left: 0,
2830
+ width: 0,
2831
+ height: 0
2832
+ });
2833
+
2834
+ // The documentElement is completely invisible.
2835
+ expect(rect(records[0].boundingClientRect))
2836
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2837
+ expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
2838
+ expect(records[0].isIntersecting).to.be(false);
2839
+ expect(records[0].intersectionRatio).to.be(0);
2840
+
2841
+ // The document.body is completely invisible.
2842
+ expect(rect(records[1].boundingClientRect))
2843
+ .to.eql(rect(body.getBoundingClientRect()));
2844
+ expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
2845
+ expect(records[1].isIntersecting).to.be(false);
2846
+ expect(records[1].intersectionRatio).to.be(0);
2847
+
2848
+ // The target1 is completely invisible.
2849
+ var clientRect1 = rect({
2850
+ top: 0,
2851
+ left: 0,
2852
+ width: bodyWidth,
2853
+ height: 200
2854
+ });
2855
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2856
+ expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
2857
+ expect(records[2].isIntersecting).to.be(false);
2858
+ expect(records[2].intersectionRatio).to.be(0);
2859
+
2860
+ done();
2861
+ io.disconnect();
2862
+ }, {}, parentRect);
2863
+ io.observe(documentElement);
2864
+ io.observe(body);
2865
+ io.observe(iframeTargetEl1);
2866
+ });
2867
+
2868
+ it('blocks until crossOriginUpdater is called first time', function(done) {
2869
+ if (supportsNativeIntersectionObserver(iframeWin)) {
2870
+ // Skip: not possible to emulate with the native observer.
2871
+ done();
2872
+ return;
2873
+ }
2874
+
2875
+ var spy = sinon.spy();
2876
+
2877
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2878
+
2879
+ var io = createObserver(spy, {});
2880
+ io.observe(iframeTargetEl1);
2881
+
2882
+ runSequence([
2883
+ function(done) {
2884
+ setTimeout(function() {
2885
+ expect(spy.callCount).to.be(0);
2886
+
2887
+ // Issue the first update.
2888
+ crossOriginUpdater(parentRect, null);
2889
+
2890
+ done();
2891
+ }, ASYNC_TIMEOUT);
2892
+ },
2893
+ function(done) {
2894
+ setTimeout(function() {
2895
+ expect(spy.callCount).to.be(1);
2896
+ var records = sortRecords(spy.lastCall.args[0]);
2897
+ expect(records.length).to.be(1);
2898
+ expect(records[0].intersectionRatio).to.be(0);
2899
+ expect(records[0].isIntersecting).to.be(false);
2900
+ done();
2901
+ }, ASYNC_TIMEOUT);
2902
+ },
2903
+ function(done) {
2904
+ io.disconnect();
2905
+ done();
2906
+ }
2907
+ ], done);
2908
+ });
2909
+
2910
+ it('doesn\'t block with a root specified', function(done) {
2911
+ var spy = sinon.spy();
2912
+
2913
+ var io = createObserver(spy, {root: body});
2914
+ io.observe(iframeTargetEl1);
2915
+
2916
+ runSequence([
2917
+ function(done) {
2918
+ setTimeout(function() {
2919
+ expect(spy.callCount).to.be(1);
2920
+ var record = sortRecords(spy.lastCall.args[0])[0];
2921
+ expect(record.intersectionRatio).to.be(1);
2922
+ expect(record.isIntersecting).to.be(true);
2923
+ expect(rect(record.rootBounds)).to.eql(rect(body.getBoundingClientRect()));
2924
+ done();
2925
+ }, ASYNC_TIMEOUT);
2926
+ },
2927
+ function(done) {
2928
+ io.disconnect();
2929
+ done();
2930
+ }
2931
+ ], done);
2932
+ });
2933
+
2934
+ it('handles style changes', function(done) {
2935
+ var spy = sinon.spy();
2936
+
2937
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2938
+
2939
+ // When first element becomes invisible, the second element will show.
2940
+ // And in reverse: when the first element becomes visible again, the
2941
+ // second element will disappear.
2942
+ var io = createObserver(spy, {}, parentRect);
2943
+ io.observe(iframeTargetEl1);
2944
+ io.observe(iframeTargetEl2);
2945
+
2946
+ runSequence([
2947
+ function(done) {
2948
+ setTimeout(function() {
2949
+ expect(spy.callCount).to.be(1);
2950
+ var records = sortRecords(spy.lastCall.args[0]);
2951
+ expect(records.length).to.be(2);
2952
+ expect(records[0].intersectionRatio).to.be(1);
2953
+ expect(records[0].isIntersecting).to.be(true);
2954
+ expect(records[1].intersectionRatio).to.be(0);
2955
+ expect(records[1].isIntersecting).to.be(false);
2956
+ done();
2957
+ }, ASYNC_TIMEOUT);
2958
+ },
2959
+ function(done) {
2960
+ iframeTargetEl1.style.display = 'none';
2961
+ setTimeout(function() {
2962
+ expect(spy.callCount).to.be(2);
2963
+ var records = sortRecords(spy.lastCall.args[0]);
2964
+ expect(records.length).to.be(2);
2965
+ expect(records[0].intersectionRatio).to.be(0);
2966
+ expect(records[0].isIntersecting).to.be(false);
2967
+ expect(records[1].intersectionRatio).to.be(1);
2968
+ expect(records[1].isIntersecting).to.be(true);
2969
+ done();
2970
+ }, ASYNC_TIMEOUT);
2971
+ },
2972
+ function(done) {
2973
+ iframeTargetEl1.style.display = '';
2974
+ setTimeout(function() {
2975
+ expect(spy.callCount).to.be(3);
2976
+ var records = sortRecords(spy.lastCall.args[0]);
2977
+ expect(records.length).to.be(2);
2978
+ expect(records[0].intersectionRatio).to.be(1);
2979
+ expect(records[0].isIntersecting).to.be(true);
2980
+ expect(records[1].intersectionRatio).to.be(0);
2981
+ expect(records[1].isIntersecting).to.be(false);
2982
+ done();
2983
+ }, ASYNC_TIMEOUT);
2984
+ },
2985
+ function(done) {
2986
+ io.disconnect();
2987
+ done();
2988
+ }
2989
+ ], done);
2990
+ });
2991
+
2992
+ it('handles scroll changes', function(done) {
2993
+ var spy = sinon.spy();
2994
+
2995
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2996
+
2997
+ // Scrolling to the middle of the iframe shows the second box and
2998
+ // hides the first.
2999
+ var io = createObserver(spy, {}, parentRect);
3000
+ io.observe(iframeTargetEl1);
3001
+ io.observe(iframeTargetEl2);
3002
+
3003
+ runSequence([
3004
+ function(done) {
3005
+ setTimeout(function() {
3006
+ expect(spy.callCount).to.be(1);
3007
+ var records = sortRecords(spy.lastCall.args[0]);
3008
+ expect(records.length).to.be(2);
3009
+ expect(records[0].intersectionRatio).to.be(1);
3010
+ expect(records[0].isIntersecting).to.be(true);
3011
+ expect(records[1].intersectionRatio).to.be(0);
3012
+ expect(records[1].isIntersecting).to.be(false);
3013
+ done();
3014
+ }, ASYNC_TIMEOUT);
3015
+ },
3016
+ function(done) {
3017
+ iframeWin.scrollTo(0, 202);
3018
+ setTimeout(function() {
3019
+ expect(spy.callCount).to.be(2);
3020
+ var records = sortRecords(spy.lastCall.args[0]);
3021
+ expect(records.length).to.be(2);
3022
+ expect(records[0].intersectionRatio).to.be(0);
3023
+ expect(records[0].isIntersecting).to.be(false);
3024
+ expect(records[1].intersectionRatio).to.be(1);
3025
+ expect(records[1].isIntersecting).to.be(true);
3026
+ done();
3027
+ }, ASYNC_TIMEOUT);
3028
+ },
3029
+ function(done) {
3030
+ iframeWin.scrollTo(0, 0);
3031
+ setTimeout(function() {
3032
+ expect(spy.callCount).to.be(3);
3033
+ var records = sortRecords(spy.lastCall.args[0]);
3034
+ expect(records.length).to.be(2);
3035
+ expect(records[0].intersectionRatio).to.be(1);
3036
+ expect(records[0].isIntersecting).to.be(true);
3037
+ expect(records[1].intersectionRatio).to.be(0);
3038
+ expect(records[1].isIntersecting).to.be(false);
3039
+ done();
3040
+ }, ASYNC_TIMEOUT);
3041
+ },
3042
+ function(done) {
3043
+ io.disconnect();
3044
+ done();
3045
+ }
3046
+ ], done);
3047
+ });
3048
+
3049
+ it('handles parent rect changes', function(done) {
3050
+ var spy = sinon.spy();
3051
+
3052
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
3053
+
3054
+ // Iframe goes off screen and returns.
3055
+ var io = createObserver(spy, {}, parentRect);
3056
+ io.observe(iframeTargetEl1);
3057
+ io.observe(iframeTargetEl2);
3058
+
3059
+ runSequence([
3060
+ function(done) {
3061
+ setTimeout(function() {
3062
+ expect(spy.callCount).to.be(1);
3063
+ var records = sortRecords(spy.lastCall.args[0]);
3064
+ expect(records.length).to.be(2);
3065
+ checkRootBoundsAreNull(records);
3066
+ expect(records[0].intersectionRatio).to.be(1);
3067
+ expect(records[0].isIntersecting).to.be(true);
3068
+ expect(records[1].intersectionRatio).to.be(0);
3069
+ expect(records[1].isIntersecting).to.be(false);
3070
+ // Top-level bounds.
3071
+ expect(records[0].intersectionRect.height).to.be(200);
3072
+ done();
3073
+ }, ASYNC_TIMEOUT);
3074
+ },
3075
+ function(done) {
3076
+ // Completely off screen.
3077
+ applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
3078
+ setTimeout(function() {
3079
+ expect(spy.callCount).to.be(2);
3080
+ var records = sortRecords(spy.lastCall.args[0]);
3081
+ expect(records.length).to.be(1);
3082
+ checkRootBoundsAreNull(records);
3083
+ expect(records[0].intersectionRatio).to.be(0);
3084
+ expect(records[0].isIntersecting).to.be(false);
3085
+ // Top-level bounds.
3086
+ expect(records[0].intersectionRect.height).to.be(0);
3087
+ done();
3088
+ }, ASYNC_TIMEOUT);
3089
+ },
3090
+ function(done) {
3091
+ // Partially returns.
3092
+ applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
3093
+ setTimeout(function() {
3094
+ expect(spy.callCount).to.be(3);
3095
+ var records = sortRecords(spy.lastCall.args[0]);
3096
+ expect(records.length).to.be(1);
3097
+ checkRootBoundsAreNull(records);
3098
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
3099
+ expect(records[0].isIntersecting).to.be(true);
3100
+ // Top-level bounds.
3101
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
3102
+ done();
3103
+ }, ASYNC_TIMEOUT);
3104
+ },
3105
+ function(done) {
3106
+ io.disconnect();
3107
+ done();
3108
+ }
3109
+ ], done);
3110
+ });
3111
+
3112
+ it('handles tracking iframe viewport', function(done) {
3113
+ iframe.style.height = '100px';
3114
+ iframe.style.top = '100px';
3115
+ iframeWin.scrollTo(0, 110);
3116
+ // {root:iframeDoc} means to track the iframe viewport.
3117
+ var io = createObserver(
3118
+ function (records) {
3119
+ io.unobserve(iframeTargetEl1);
3120
+ var intersectionRect = rect({
3121
+ top: 0, // if root=null, then this would be 100.
3122
+ left: 0,
3123
+ height: 90,
3124
+ width: bodyWidth
3125
+ });
3126
+ expect(records.length).to.be(1);
3127
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
3128
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3129
+ done();
3130
+ },
3131
+ { root: iframeDoc }
3132
+ );
3133
+
3134
+ io.observe(iframeTargetEl1);
3135
+ });
3136
+
3137
+ it('handles tracking iframe viewport with rootMargin', function(done) {
3138
+ iframe.style.height = '100px';
3139
+
3140
+ var io = createObserver(
3141
+ function (records) {
3142
+ io.unobserve(iframeTargetEl1);
3143
+ var intersectionRect = rect({
3144
+ top: 0, // if root=null, then this would be 100.
3145
+ left: 0,
3146
+ height: 200,
3147
+ width: bodyWidth
3148
+ });
3149
+
3150
+ // rootMargin: 100% --> 3x width + 3x height.
3151
+ var expectedRootBounds = rect({
3152
+ top: -100,
3153
+ left: -bodyWidth,
3154
+ width: bodyWidth * 3,
3155
+ height: 100 * 3
3156
+ });
3157
+ expect(records.length).to.be(1);
3158
+ expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
3159
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3160
+ done();
3161
+ },
3162
+ { root: iframeDoc, rootMargin: '100%' }
3163
+ );
3164
+
3165
+ io.observe(iframeTargetEl1);
3166
+ });
3167
+ });
3168
+ });
920
3169
  });
921
3170
 
922
3171
 
@@ -943,11 +3192,13 @@ function runSequence(functions, done) {
943
3192
  /**
944
3193
  * Returns whether or not the current browser has native support for
945
3194
  * IntersectionObserver.
3195
+ * @param {Window=} win
946
3196
  * @return {boolean} True if native support is detected.
947
3197
  */
948
- function supportsNativeIntersectionObserver() {
949
- return 'IntersectionObserver' in window &&
950
- window.IntersectionObserver.toString().indexOf('[native code]') > -1;
3198
+ function supportsNativeIntersectionObserver(win) {
3199
+ win = win || window;
3200
+ return 'IntersectionObserver' in win &&
3201
+ win.IntersectionObserver.toString().indexOf('[native code]') > -1;
951
3202
  }
952
3203
 
953
3204