intersection-observer 0.5.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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