intersection-observer 0.6.0 → 0.10.0

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.
@@ -157,6 +157,21 @@ describe('IntersectionObserver', function() {
157
157
  }).to.throwException();
158
158
  });
159
159
 
160
+ it('fills in x and y in the resulting rects', function(done) {
161
+ io = new IntersectionObserver(function(records) {
162
+ expect(records.length).to.be(1);
163
+ var entry = records[0];
164
+ expect(entry.rootBounds.x).to.be(entry.rootBounds.left);
165
+ expect(entry.rootBounds.y).to.be(entry.rootBounds.top);
166
+ expect(entry.boundingClientRect.x).to.be(entry.boundingClientRect.left);
167
+ expect(entry.boundingClientRect.y).to.be(entry.boundingClientRect.top);
168
+ expect(entry.intersectionRect.x).to.be(entry.intersectionRect.left);
169
+ expect(entry.intersectionRect.y).to.be(entry.intersectionRect.top);
170
+ done();
171
+ }, {root: rootEl});
172
+ targetEl2.style.top = '-40px';
173
+ io.observe(targetEl1);
174
+ });
160
175
 
161
176
  it('triggers for all targets when observing begins', function(done) {
162
177
  io = new IntersectionObserver(function(records) {
@@ -941,6 +956,1413 @@ describe('IntersectionObserver', function() {
941
956
 
942
957
  });
943
958
 
959
+ describe('iframe', function() {
960
+ var iframe;
961
+ var documentElement, body;
962
+ var iframeTargetEl1, iframeTargetEl2;
963
+ var bodyWidth;
964
+
965
+ beforeEach(function(done) {
966
+ iframe = document.createElement('iframe');
967
+ iframe.setAttribute('frameborder', '0');
968
+ iframe.setAttribute('scrolling', 'yes');
969
+ iframe.style.position = 'fixed';
970
+ iframe.style.top = '0px';
971
+ iframe.style.width = '100px';
972
+ iframe.style.height = '200px';
973
+ iframe.onerror = function() {
974
+ done(new Error('iframe initialization failed'));
975
+ };
976
+ iframe.onload = function() {
977
+ iframe.onload = null;
978
+ iframeWin = iframe.contentWindow;
979
+ iframeDoc = iframeWin.document;
980
+ iframeDoc.open();
981
+ iframeDoc.write('<!DOCTYPE html><html><body>');
982
+ iframeDoc.write('<style>');
983
+ iframeDoc.write('body {margin: 0}');
984
+ iframeDoc.write('.target {height: 200px; margin-bottom: 2px; background: blue;}');
985
+ iframeDoc.write('</style>');
986
+ iframeDoc.close();
987
+
988
+ // Ensure the documentElement and body are always sorted on top. See
989
+ // `sortRecords` for more info.
990
+ documentElement = iframeDoc.documentElement;
991
+ body = iframeDoc.body;
992
+ documentElement.id = 'A1';
993
+ body.id = 'A1';
994
+
995
+ function createTarget(id, bg) {
996
+ var target = iframeDoc.createElement('div');
997
+ target.id = id;
998
+ target.className = 'target';
999
+ target.style.background = bg;
1000
+ iframeDoc.body.appendChild(target);
1001
+ return target;
1002
+ }
1003
+ iframeTargetEl1 = createTarget('target1', 'blue');
1004
+ iframeTargetEl2 = createTarget('target2', 'green');
1005
+ bodyWidth = iframeDoc.body.clientWidth;
1006
+ done();
1007
+ };
1008
+ iframe.src = 'about:blank';
1009
+ rootEl.appendChild(iframe);
1010
+ });
1011
+
1012
+ afterEach(function() {
1013
+ rootEl.removeChild(iframe);
1014
+ });
1015
+
1016
+ function rect(r) {
1017
+ return {
1018
+ y: typeof r.y == 'number' ? r.y : r.top,
1019
+ x: typeof r.x == 'number' ? r.x : r.left,
1020
+ top: r.top,
1021
+ left: r.left,
1022
+ width: r.width != null ? r.width : r.right - r.left,
1023
+ height: r.height != null ? r.height : r.bottom - r.top,
1024
+ right: r.right != null ? r.right : r.left + r.width,
1025
+ bottom: r.bottom != null ? r.bottom : r.top + r.height
1026
+ };
1027
+ }
1028
+
1029
+ function getRootRect(doc) {
1030
+ var html = doc.documentElement;
1031
+ var body = doc.body;
1032
+ return rect({
1033
+ top: 0,
1034
+ left: 0,
1035
+ right: html.clientWidth || body.clientWidth,
1036
+ width: html.clientWidth || body.clientWidth,
1037
+ bottom: html.clientHeight || body.clientHeight,
1038
+ height: html.clientHeight || body.clientHeight
1039
+ });
1040
+ }
1041
+
1042
+ describe('same-origin iframe', function() {
1043
+ it('iframe targets do not intersect with a top root element', function(done) {
1044
+ var io = new IntersectionObserver(function(unsortedRecords) {
1045
+ var records = sortRecords(unsortedRecords);
1046
+ expect(records.length).to.be(2);
1047
+ expect(records[0].isIntersecting).to.be(false);
1048
+ expect(records[1].isIntersecting).to.be(false);
1049
+ done();
1050
+ io.disconnect();
1051
+ }, {root: rootEl});
1052
+ io.observe(iframeTargetEl1);
1053
+ io.observe(iframeTargetEl2);
1054
+ });
1055
+
1056
+ it('triggers for all targets in top-level root', function(done) {
1057
+ var io = new IntersectionObserver(function(unsortedRecords) {
1058
+ var records = sortRecords(unsortedRecords);
1059
+ expect(records.length).to.be(2);
1060
+ expect(records[0].isIntersecting).to.be(true);
1061
+ expect(records[0].intersectionRatio).to.be(1);
1062
+ expect(records[1].isIntersecting).to.be(false);
1063
+ expect(records[1].intersectionRatio).to.be(0);
1064
+
1065
+ // The rootBounds is for the document's root.
1066
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1067
+
1068
+ done();
1069
+ io.disconnect();
1070
+ });
1071
+ io.observe(iframeTargetEl1);
1072
+ io.observe(iframeTargetEl2);
1073
+ });
1074
+
1075
+ it('triggers for all targets in iframe-level root', function(done) {
1076
+ var io = new IntersectionObserver(function(unsortedRecords) {
1077
+ var records = sortRecords(unsortedRecords);
1078
+ expect(records.length).to.be(2);
1079
+ expect(records[0].intersectionRatio).to.be(1);
1080
+ expect(records[1].intersectionRatio).to.be(1);
1081
+
1082
+ // The rootBounds is for the document's root.
1083
+ expect(rect(records[0].rootBounds)).
1084
+ to.eql(rect(iframeDoc.body.getBoundingClientRect()));
1085
+
1086
+ done();
1087
+ io.disconnect();
1088
+ }, {root: iframeDoc.body});
1089
+ io.observe(iframeTargetEl1);
1090
+ io.observe(iframeTargetEl2);
1091
+ });
1092
+
1093
+ it('calculates rects for a fully visible frame', function(done) {
1094
+ iframe.style.top = '0px';
1095
+ iframe.style.height = '300px';
1096
+ var io = new IntersectionObserver(function(unsortedRecords) {
1097
+ var records = sortRecords(unsortedRecords);
1098
+ expect(records.length).to.be(2);
1099
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1100
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1101
+
1102
+ // The target1 is fully visible.
1103
+ var clientRect1 = rect({
1104
+ top: 0,
1105
+ left: 0,
1106
+ width: bodyWidth,
1107
+ height: 200
1108
+ });
1109
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1110
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1111
+ expect(records[0].isIntersecting).to.be(true);
1112
+ expect(records[0].intersectionRatio).to.be(1);
1113
+
1114
+ // The target2 is partially visible.
1115
+ var clientRect2 = rect({
1116
+ top: 202,
1117
+ left: 0,
1118
+ width: bodyWidth,
1119
+ height: 200
1120
+ });
1121
+ var intersectRect2 = rect({
1122
+ top: 202,
1123
+ left: 0,
1124
+ width: bodyWidth,
1125
+ // The bottom is clipped off.
1126
+ bottom: 300
1127
+ });
1128
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1129
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1130
+ expect(records[1].isIntersecting).to.be(true);
1131
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
1132
+
1133
+ done();
1134
+ io.disconnect();
1135
+ });
1136
+ io.observe(iframeTargetEl1);
1137
+ io.observe(iframeTargetEl2);
1138
+ });
1139
+
1140
+ it('calculates rects for a fully visible and offset frame', function(done) {
1141
+ iframe.style.top = '10px';
1142
+ iframe.style.height = '300px';
1143
+ var io = new IntersectionObserver(function(unsortedRecords) {
1144
+ var records = sortRecords(unsortedRecords);
1145
+ expect(records.length).to.be(2);
1146
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1147
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1148
+
1149
+ // The target1 is fully visible.
1150
+ var clientRect1 = rect({
1151
+ top: 0,
1152
+ left: 0,
1153
+ width: bodyWidth,
1154
+ height: 200
1155
+ });
1156
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1157
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1158
+ expect(records[0].isIntersecting).to.be(true);
1159
+ expect(records[0].intersectionRatio).to.be(1);
1160
+
1161
+ // The target2 is partially visible.
1162
+ var clientRect2 = rect({
1163
+ top: 202,
1164
+ left: 0,
1165
+ width: bodyWidth,
1166
+ height: 200
1167
+ });
1168
+ var intersectRect2 = rect({
1169
+ top: 202,
1170
+ left: 0,
1171
+ width: bodyWidth,
1172
+ // The bottom is clipped off.
1173
+ bottom: 300
1174
+ });
1175
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1176
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1177
+ expect(records[1].isIntersecting).to.be(true);
1178
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5
1179
+
1180
+ done();
1181
+ io.disconnect();
1182
+ });
1183
+ io.observe(iframeTargetEl1);
1184
+ io.observe(iframeTargetEl2);
1185
+ });
1186
+
1187
+ it('calculates rects for a clipped frame on top', function(done) {
1188
+ iframe.style.top = '-10px';
1189
+ iframe.style.height = '300px';
1190
+ var io = new IntersectionObserver(function(unsortedRecords) {
1191
+ var records = sortRecords(unsortedRecords);
1192
+ expect(records.length).to.be(2);
1193
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1194
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1195
+
1196
+ // The target1 is clipped at the top by the iframe's clipping.
1197
+ var clientRect1 = rect({
1198
+ top: 0,
1199
+ left: 0,
1200
+ width: bodyWidth,
1201
+ height: 200
1202
+ });
1203
+ var intersectRect1 = rect({
1204
+ left: 0,
1205
+ width: bodyWidth,
1206
+ // Top is clipped.
1207
+ top: 10,
1208
+ height: 200 - 10
1209
+ });
1210
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1211
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1212
+ expect(records[0].isIntersecting).to.be(true);
1213
+ expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1214
+
1215
+ // The target2 is partially visible.
1216
+ var clientRect2 = rect({
1217
+ top: 202,
1218
+ left: 0,
1219
+ width: bodyWidth,
1220
+ height: 200
1221
+ });
1222
+ var intersectRect2 = rect({
1223
+ top: 202,
1224
+ left: 0,
1225
+ width: bodyWidth,
1226
+ // The bottom is clipped off.
1227
+ bottom: 300
1228
+ });
1229
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1230
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1231
+ expect(records[1].isIntersecting).to.be(true);
1232
+ expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.49
1233
+
1234
+ done();
1235
+ io.disconnect();
1236
+ });
1237
+ io.observe(iframeTargetEl1);
1238
+ io.observe(iframeTargetEl2);
1239
+ });
1240
+
1241
+ it('calculates rects for a clipped frame on bottom', function(done) {
1242
+ iframe.style.top = 'auto';
1243
+ iframe.style.bottom = '-10px';
1244
+ iframe.style.height = '300px';
1245
+ var io = new IntersectionObserver(function(unsortedRecords) {
1246
+ var records = sortRecords(unsortedRecords);
1247
+ expect(records.length).to.be(2);
1248
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1249
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1250
+
1251
+ // The target1 is clipped at the top by the iframe's clipping.
1252
+ var clientRect1 = rect({
1253
+ top: 0,
1254
+ left: 0,
1255
+ width: bodyWidth,
1256
+ height: 200
1257
+ });
1258
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1259
+ expect(rect(records[0].intersectionRect)).to.eql(clientRect1);
1260
+ expect(records[0].isIntersecting).to.be(true);
1261
+ expect(records[0].intersectionRatio).to.be(1);
1262
+
1263
+ // The target2 is partially visible.
1264
+ var clientRect2 = rect({
1265
+ top: 202,
1266
+ left: 0,
1267
+ width: bodyWidth,
1268
+ height: 200
1269
+ });
1270
+ var intersectRect2 = rect({
1271
+ top: 202,
1272
+ left: 0,
1273
+ width: bodyWidth,
1274
+ // The bottom is clipped off.
1275
+ bottom: 300 - 10
1276
+ });
1277
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1278
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1279
+ expect(records[1].isIntersecting).to.be(true);
1280
+ expect(records[1].intersectionRatio).to.be.within(0.43, 0.45); // ~0.44
1281
+
1282
+ done();
1283
+ io.disconnect();
1284
+ });
1285
+ io.observe(iframeTargetEl1);
1286
+ io.observe(iframeTargetEl2);
1287
+ });
1288
+
1289
+ it('calculates rects for a fully visible frame and scrolled', function(done) {
1290
+ iframe.style.top = '0px';
1291
+ iframe.style.height = '300px';
1292
+ iframeWin.scrollTo(0, 10);
1293
+ var io = new IntersectionObserver(function(unsortedRecords) {
1294
+ var records = sortRecords(unsortedRecords);
1295
+ expect(records.length).to.be(2);
1296
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1297
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1298
+
1299
+ // The target1 is fully visible.
1300
+ var clientRect1 = rect({
1301
+ top: -10,
1302
+ left: 0,
1303
+ width: bodyWidth,
1304
+ height: 200
1305
+ });
1306
+ var intersectRect1 = rect({
1307
+ top: 0,
1308
+ left: 0,
1309
+ width: bodyWidth,
1310
+ // Height is only for the visible area.
1311
+ height: 200 - 10
1312
+ });
1313
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1314
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1315
+ expect(records[0].isIntersecting).to.be(true);
1316
+ expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1317
+
1318
+ // The target2 is partially visible.
1319
+ var clientRect2 = rect({
1320
+ top: 202 - 10,
1321
+ left: 0,
1322
+ width: bodyWidth,
1323
+ height: 200
1324
+ });
1325
+ var intersectRect2 = rect({
1326
+ top: 202 - 10,
1327
+ left: 0,
1328
+ width: bodyWidth,
1329
+ // The bottom is clipped off.
1330
+ bottom: 300
1331
+ });
1332
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1333
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1334
+ expect(records[1].isIntersecting).to.be(true);
1335
+ expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
1336
+
1337
+ done();
1338
+ io.disconnect();
1339
+ });
1340
+ io.observe(iframeTargetEl1);
1341
+ io.observe(iframeTargetEl2);
1342
+ });
1343
+
1344
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
1345
+ iframe.style.top = '-10px';
1346
+ iframe.style.height = '300px';
1347
+ iframeWin.scrollTo(0, 10);
1348
+ var io = new IntersectionObserver(function(unsortedRecords) {
1349
+ var records = sortRecords(unsortedRecords);
1350
+ expect(records.length).to.be(2);
1351
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(document));
1352
+ expect(rect(records[1].rootBounds)).to.eql(getRootRect(document));
1353
+
1354
+ // The target1 is clipped at the top by the iframe's clipping.
1355
+ var clientRect1 = rect({
1356
+ top: -10,
1357
+ left: 0,
1358
+ width: bodyWidth,
1359
+ height: 200
1360
+ });
1361
+ var intersectRect1 = rect({
1362
+ left: 0,
1363
+ width: bodyWidth,
1364
+ // Top is clipped.
1365
+ top: 10,
1366
+ // The height is less by both: offset and scroll.
1367
+ height: 200 - 10 - 10
1368
+ });
1369
+ expect(rect(records[0].boundingClientRect)).to.eql(clientRect1);
1370
+ expect(rect(records[0].intersectionRect)).to.eql(intersectRect1);
1371
+ expect(records[0].isIntersecting).to.be(true);
1372
+ expect(records[0].intersectionRatio).to.within(0.89, 0.91); // ~0.9
1373
+
1374
+ // The target2 is partially visible.
1375
+ var clientRect2 = rect({
1376
+ top: 202 - 10,
1377
+ left: 0,
1378
+ width: bodyWidth,
1379
+ height: 200
1380
+ });
1381
+ var intersectRect2 = rect({
1382
+ top: 202 - 10,
1383
+ left: 0,
1384
+ width: bodyWidth,
1385
+ // The bottom is clipped off.
1386
+ bottom: 300
1387
+ });
1388
+ expect(rect(records[1].boundingClientRect)).to.eql(clientRect2);
1389
+ expect(rect(records[1].intersectionRect)).to.eql(intersectRect2);
1390
+ expect(records[1].isIntersecting).to.be(true);
1391
+ expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
1392
+
1393
+ done();
1394
+ io.disconnect();
1395
+ });
1396
+ io.observe(iframeTargetEl1);
1397
+ io.observe(iframeTargetEl2);
1398
+ });
1399
+
1400
+ it('handles style changes', function(done) {
1401
+ var spy = sinon.spy();
1402
+
1403
+ // When first element becomes invisible, the second element will show.
1404
+ // And in reverse: when the first element becomes visible again, the
1405
+ // second element will disappear.
1406
+ var io = new IntersectionObserver(spy);
1407
+ io.observe(iframeTargetEl1);
1408
+ io.observe(iframeTargetEl2);
1409
+
1410
+ runSequence([
1411
+ function(done) {
1412
+ setTimeout(function() {
1413
+ expect(spy.callCount).to.be(1);
1414
+ var records = sortRecords(spy.lastCall.args[0]);
1415
+ expect(records.length).to.be(2);
1416
+ expect(records[0].intersectionRatio).to.be(1);
1417
+ expect(records[0].isIntersecting).to.be(true);
1418
+ expect(records[1].intersectionRatio).to.be(0);
1419
+ expect(records[1].isIntersecting).to.be(false);
1420
+ done();
1421
+ }, ASYNC_TIMEOUT);
1422
+ },
1423
+ function(done) {
1424
+ iframeTargetEl1.style.display = 'none';
1425
+ setTimeout(function() {
1426
+ expect(spy.callCount).to.be(2);
1427
+ var records = sortRecords(spy.lastCall.args[0]);
1428
+ expect(records.length).to.be(2);
1429
+ expect(records[0].intersectionRatio).to.be(0);
1430
+ expect(records[0].isIntersecting).to.be(false);
1431
+ expect(records[1].intersectionRatio).to.be(1);
1432
+ expect(records[1].isIntersecting).to.be(true);
1433
+ done();
1434
+ }, ASYNC_TIMEOUT);
1435
+ },
1436
+ function(done) {
1437
+ iframeTargetEl1.style.display = '';
1438
+ setTimeout(function() {
1439
+ expect(spy.callCount).to.be(3);
1440
+ var records = sortRecords(spy.lastCall.args[0]);
1441
+ expect(records.length).to.be(2);
1442
+ expect(records[0].intersectionRatio).to.be(1);
1443
+ expect(records[0].isIntersecting).to.be(true);
1444
+ expect(records[1].intersectionRatio).to.be(0);
1445
+ expect(records[1].isIntersecting).to.be(false);
1446
+ done();
1447
+ }, ASYNC_TIMEOUT);
1448
+ },
1449
+ function(done) {
1450
+ io.disconnect();
1451
+ done();
1452
+ }
1453
+ ], done);
1454
+ });
1455
+
1456
+ it('handles scroll changes', function(done) {
1457
+ var spy = sinon.spy();
1458
+
1459
+ // Scrolling to the middle of the iframe shows the second box and
1460
+ // hides the first.
1461
+ var io = new IntersectionObserver(spy);
1462
+ io.observe(iframeTargetEl1);
1463
+ io.observe(iframeTargetEl2);
1464
+
1465
+ runSequence([
1466
+ function(done) {
1467
+ setTimeout(function() {
1468
+ expect(spy.callCount).to.be(1);
1469
+ var records = sortRecords(spy.lastCall.args[0]);
1470
+ expect(records.length).to.be(2);
1471
+ expect(records[0].intersectionRatio).to.be(1);
1472
+ expect(records[0].isIntersecting).to.be(true);
1473
+ expect(records[1].intersectionRatio).to.be(0);
1474
+ expect(records[1].isIntersecting).to.be(false);
1475
+ done();
1476
+ }, ASYNC_TIMEOUT);
1477
+ },
1478
+ function(done) {
1479
+ iframeWin.scrollTo(0, 202);
1480
+ setTimeout(function() {
1481
+ expect(spy.callCount).to.be(2);
1482
+ var records = sortRecords(spy.lastCall.args[0]);
1483
+ expect(records.length).to.be(2);
1484
+ expect(records[0].intersectionRatio).to.be(0);
1485
+ expect(records[0].isIntersecting).to.be(false);
1486
+ expect(records[1].intersectionRatio).to.be(1);
1487
+ expect(records[1].isIntersecting).to.be(true);
1488
+ done();
1489
+ }, ASYNC_TIMEOUT);
1490
+ },
1491
+ function(done) {
1492
+ iframeWin.scrollTo(0, 0);
1493
+ setTimeout(function() {
1494
+ expect(spy.callCount).to.be(3);
1495
+ var records = sortRecords(spy.lastCall.args[0]);
1496
+ expect(records.length).to.be(2);
1497
+ expect(records[0].intersectionRatio).to.be(1);
1498
+ expect(records[0].isIntersecting).to.be(true);
1499
+ expect(records[1].intersectionRatio).to.be(0);
1500
+ expect(records[1].isIntersecting).to.be(false);
1501
+ done();
1502
+ }, ASYNC_TIMEOUT);
1503
+ },
1504
+ function(done) {
1505
+ io.disconnect();
1506
+ done();
1507
+ }
1508
+ ], done);
1509
+ });
1510
+
1511
+ it('handles iframe changes', function(done) {
1512
+ var spy = sinon.spy();
1513
+
1514
+ // Iframe goes off screen and returns.
1515
+ var io = new IntersectionObserver(spy);
1516
+ io.observe(iframeTargetEl1);
1517
+ io.observe(iframeTargetEl2);
1518
+
1519
+ runSequence([
1520
+ function(done) {
1521
+ setTimeout(function() {
1522
+ expect(spy.callCount).to.be(1);
1523
+ var records = sortRecords(spy.lastCall.args[0]);
1524
+ expect(records.length).to.be(2);
1525
+ expect(records[0].intersectionRatio).to.be(1);
1526
+ expect(records[0].isIntersecting).to.be(true);
1527
+ expect(records[1].intersectionRatio).to.be(0);
1528
+ expect(records[1].isIntersecting).to.be(false);
1529
+ // Top-level bounds.
1530
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1531
+ expect(records[0].intersectionRect.height).to.be(200);
1532
+ done();
1533
+ }, ASYNC_TIMEOUT);
1534
+ },
1535
+ function(done) {
1536
+ // Completely off screen.
1537
+ iframe.style.top = '-202px';
1538
+ setTimeout(function() {
1539
+ expect(spy.callCount).to.be(2);
1540
+ var records = sortRecords(spy.lastCall.args[0]);
1541
+ expect(records.length).to.be(1);
1542
+ expect(records[0].intersectionRatio).to.be(0);
1543
+ expect(records[0].isIntersecting).to.be(false);
1544
+ // Top-level bounds.
1545
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1546
+ expect(records[0].intersectionRect.height).to.be(0);
1547
+ done();
1548
+ }, ASYNC_TIMEOUT);
1549
+ },
1550
+ function(done) {
1551
+ // Partially returns.
1552
+ iframe.style.top = '-100px';
1553
+ setTimeout(function() {
1554
+ expect(spy.callCount).to.be(3);
1555
+ var records = sortRecords(spy.lastCall.args[0]);
1556
+ expect(records.length).to.be(1);
1557
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
1558
+ expect(records[0].isIntersecting).to.be(true);
1559
+ // Top-level bounds.
1560
+ expect(records[0].rootBounds.height).to.be(innerHeight);
1561
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
1562
+ done();
1563
+ }, ASYNC_TIMEOUT);
1564
+ },
1565
+ function(done) {
1566
+ io.disconnect();
1567
+ done();
1568
+ }
1569
+ ], done);
1570
+ });
1571
+
1572
+ it('continues to monitor until the last target unobserved', function(done) {
1573
+ var spy = sinon.spy();
1574
+
1575
+ // Iframe goes off screen and returns.
1576
+ var io = new IntersectionObserver(spy);
1577
+ io.observe(target1);
1578
+ io.observe(iframeTargetEl1);
1579
+ io.observe(iframeTargetEl2);
1580
+
1581
+ runSequence([
1582
+ function(done) {
1583
+ setTimeout(function() {
1584
+ expect(spy.callCount).to.be(1);
1585
+ expect(spy.lastCall.args[0].length).to.be(3);
1586
+
1587
+ // Unobserve one from the main context and one from iframe.
1588
+ io.unobserve(target1);
1589
+ io.unobserve(iframeTargetEl2);
1590
+
1591
+ done();
1592
+ }, ASYNC_TIMEOUT);
1593
+ },
1594
+ function(done) {
1595
+ // Completely off screen.
1596
+ iframe.style.top = '-202px';
1597
+ setTimeout(function() {
1598
+ expect(spy.callCount).to.be(2);
1599
+ expect(spy.lastCall.args[0].length).to.be(1);
1600
+
1601
+ io.unobserve(iframeTargetEl1);
1602
+
1603
+ done();
1604
+ }, ASYNC_TIMEOUT);
1605
+ },
1606
+ function(done) {
1607
+ // Partially returns.
1608
+ iframe.style.top = '-100px';
1609
+ setTimeout(function() {
1610
+ expect(spy.callCount).to.be(2);
1611
+ done();
1612
+ }, ASYNC_TIMEOUT);
1613
+ },
1614
+ function(done) {
1615
+ io.disconnect();
1616
+ done();
1617
+ }
1618
+ ], done);
1619
+ });
1620
+ });
1621
+
1622
+ describe('cross-origin iframe', function() {
1623
+ var ASYNC_TIMEOUT = 300;
1624
+ var crossOriginUpdater;
1625
+
1626
+ beforeEach(function(done) {
1627
+ Object.defineProperty(iframeWin, 'frameElement', {value: null});
1628
+
1629
+ /* Uncomment these lines to force polyfill inside the iframe.
1630
+ delete iframeWin.IntersectionObserver;
1631
+ delete iframeWin.IntersectionObserverEntry;
1632
+ */
1633
+
1634
+ // Install polyfill right into the iframe.
1635
+ if (!iframeWin.IntersectionObserver) {
1636
+ var script = iframeDoc.createElement('script');
1637
+ script.src = 'intersection-observer.js';
1638
+ script.onload = function() {
1639
+ if (iframeWin.IntersectionObserver._setupCrossOriginUpdater) {
1640
+ crossOriginUpdater = iframeWin.IntersectionObserver._setupCrossOriginUpdater();
1641
+ }
1642
+ done();
1643
+ };
1644
+ iframeDoc.body.appendChild(script);
1645
+ } else {
1646
+ done();
1647
+ }
1648
+ });
1649
+
1650
+ afterEach(function() {
1651
+ if (IntersectionObserver._resetCrossOriginUpdater) {
1652
+ IntersectionObserver._resetCrossOriginUpdater();
1653
+ }
1654
+ });
1655
+
1656
+ function computeRectIntersection(rect1, rect2) {
1657
+ var top = Math.max(rect1.top, rect2.top);
1658
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
1659
+ var left = Math.max(rect1.left, rect2.left);
1660
+ var right = Math.min(rect1.right, rect2.right);
1661
+ var width = right - left;
1662
+ var height = bottom - top;
1663
+
1664
+ return (width >= 0 && height >= 0) && {
1665
+ top: top,
1666
+ bottom: bottom,
1667
+ left: left,
1668
+ right: right,
1669
+ width: width,
1670
+ height: height
1671
+ } || {
1672
+ top: 0,
1673
+ bottom: 0,
1674
+ left: 0,
1675
+ right: 0,
1676
+ width: 0,
1677
+ height: 0
1678
+ };
1679
+ }
1680
+
1681
+ function checkRootBoundsAreNull(records) {
1682
+ if (!supportsNativeIntersectionObserver(iframeWin)) {
1683
+ records.forEach(function(record) {
1684
+ expect(record.rootBounds).to.be(null);
1685
+ });
1686
+ }
1687
+ }
1688
+
1689
+ function applyParentRect(parentRect) {
1690
+ if (crossOriginUpdater) {
1691
+ var parentIntersectionRect = computeRectIntersection(
1692
+ parentRect, getRootRect(document));
1693
+ crossOriginUpdater(parentRect, parentIntersectionRect);
1694
+ } else {
1695
+ iframe.style.top = parentRect.top + 'px';
1696
+ iframe.style.left = parentRect.left + 'px';
1697
+ iframe.style.height = parentRect.height + 'px';
1698
+ iframe.style.width = parentRect.width + 'px';
1699
+ }
1700
+ }
1701
+
1702
+ function createObserver(callback, options, parentRect) {
1703
+ var io = new iframeWin.IntersectionObserver(callback, options);
1704
+ if (parentRect) {
1705
+ applyParentRect(parentRect);
1706
+ }
1707
+ return io;
1708
+ }
1709
+
1710
+ it('calculates rects for a fully visible frame', function(done) {
1711
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
1712
+ var io = createObserver(function(unsortedRecords) {
1713
+ var records = sortRecords(unsortedRecords);
1714
+ expect(records.length).to.be(3);
1715
+ checkRootBoundsAreNull(records);
1716
+
1717
+ // The documentElement is partially visible.
1718
+ expect(rect(records[0].boundingClientRect))
1719
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1720
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1721
+ top: 0,
1722
+ left: 0,
1723
+ width: bodyWidth,
1724
+ height: 300
1725
+ }));
1726
+ expect(records[0].isIntersecting).to.be(true);
1727
+ // 300 / 404 == ~0.743
1728
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
1729
+
1730
+ // The document.body is partially visible.
1731
+ expect(rect(records[1].boundingClientRect))
1732
+ .to.eql(rect(body.getBoundingClientRect()));
1733
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1734
+ top: 0,
1735
+ left: 0,
1736
+ width: bodyWidth,
1737
+ height: 300
1738
+ }));
1739
+ expect(records[1].isIntersecting).to.be(true);
1740
+ // 300 / 402 == ~0.746
1741
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
1742
+
1743
+ // The target1 is fully visible.
1744
+ var clientRect1 = rect({
1745
+ top: 0,
1746
+ left: 0,
1747
+ width: bodyWidth,
1748
+ height: 200
1749
+ });
1750
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1751
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1752
+ expect(records[2].isIntersecting).to.be(true);
1753
+ expect(records[2].intersectionRatio).to.be(1);
1754
+
1755
+ done();
1756
+ io.disconnect();
1757
+ }, {}, parentRect);
1758
+ io.observe(documentElement);
1759
+ io.observe(body);
1760
+ io.observe(iframeTargetEl1);
1761
+ });
1762
+
1763
+ it('calculates rects for a fully visible and offset frame', function(done) {
1764
+ var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
1765
+ var io = createObserver(function(unsortedRecords) {
1766
+ var records = sortRecords(unsortedRecords);
1767
+ expect(records.length).to.be(3);
1768
+ checkRootBoundsAreNull(records);
1769
+
1770
+ // The documentElement is partially visible.
1771
+ expect(rect(records[0].boundingClientRect))
1772
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1773
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1774
+ top: 0,
1775
+ left: 0,
1776
+ width: bodyWidth,
1777
+ height: 300
1778
+ }));
1779
+ expect(records[0].isIntersecting).to.be(true);
1780
+ // 300 / 404 == ~0.743
1781
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
1782
+
1783
+ // The document.body is partially visible.
1784
+ expect(rect(records[1].boundingClientRect))
1785
+ .to.eql(rect(body.getBoundingClientRect()));
1786
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1787
+ top: 0,
1788
+ left: 0,
1789
+ width: bodyWidth,
1790
+ height: 300
1791
+ }));
1792
+ expect(records[1].isIntersecting).to.be(true);
1793
+ // 300 / 402 == ~0.746
1794
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
1795
+
1796
+ // The target1 is fully visible.
1797
+ var clientRect1 = rect({
1798
+ top: 0,
1799
+ left: 0,
1800
+ width: bodyWidth,
1801
+ height: 200
1802
+ });
1803
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1804
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1805
+ expect(records[2].isIntersecting).to.be(true);
1806
+ expect(records[2].intersectionRatio).to.be(1);
1807
+
1808
+ done();
1809
+ io.disconnect();
1810
+ }, {}, parentRect);
1811
+ io.observe(documentElement);
1812
+ io.observe(body);
1813
+ io.observe(iframeTargetEl1);
1814
+ });
1815
+
1816
+ it('calculates rects for a clipped frame on top', function(done) {
1817
+ var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
1818
+ var io = createObserver(function(unsortedRecords) {
1819
+ var records = sortRecords(unsortedRecords);
1820
+ expect(records.length).to.be(3);
1821
+ checkRootBoundsAreNull(records);
1822
+
1823
+ // The documentElement is partially visible.
1824
+ expect(rect(records[0].boundingClientRect))
1825
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1826
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1827
+ top: 10,
1828
+ left: 0,
1829
+ width: bodyWidth,
1830
+ height: 300 - 10
1831
+ }));
1832
+ expect(records[0].isIntersecting).to.be(true);
1833
+ // (300 - 10) / 404 == ~0.717
1834
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
1835
+
1836
+ // The document.body is partially visible.
1837
+ expect(rect(records[1].boundingClientRect))
1838
+ .to.eql(rect(body.getBoundingClientRect()));
1839
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1840
+ top: 10,
1841
+ left: 0,
1842
+ width: bodyWidth,
1843
+ height: 300 - 10
1844
+ }));
1845
+ expect(records[1].isIntersecting).to.be(true);
1846
+ // (300 - 10) / 402 == ~0.721
1847
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
1848
+
1849
+ // The target1 is clipped at the top by the iframe's clipping.
1850
+ var clientRect1 = rect({
1851
+ top: 0,
1852
+ left: 0,
1853
+ width: bodyWidth,
1854
+ height: 200
1855
+ });
1856
+ var intersectRect1 = rect({
1857
+ left: 0,
1858
+ width: bodyWidth,
1859
+ // Top is clipped.
1860
+ top: 10,
1861
+ height: 200 - 10
1862
+ });
1863
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1864
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
1865
+ expect(records[2].isIntersecting).to.be(true);
1866
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1867
+
1868
+ done();
1869
+ io.disconnect();
1870
+ }, {}, parentRect);
1871
+ io.observe(documentElement);
1872
+ io.observe(body);
1873
+ io.observe(iframeTargetEl1);
1874
+ });
1875
+
1876
+ it('calculates rects for a clipped frame on bottom', function(done) {
1877
+ var rootRect = getRootRect(document);
1878
+ var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
1879
+ var io = createObserver(function(unsortedRecords) {
1880
+ var records = sortRecords(unsortedRecords);
1881
+ expect(records.length).to.be(3);
1882
+ checkRootBoundsAreNull(records);
1883
+
1884
+ // The documentElement is partially visible.
1885
+ expect(rect(records[0].boundingClientRect))
1886
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1887
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1888
+ top: 0,
1889
+ left: 0,
1890
+ width: bodyWidth,
1891
+ height: 300 - 10
1892
+ }));
1893
+ expect(records[0].isIntersecting).to.be(true);
1894
+ // (300 - 10) / 404 == ~0.717
1895
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
1896
+
1897
+ // The document.body is partially visible.
1898
+ expect(rect(records[1].boundingClientRect))
1899
+ .to.eql(rect(body.getBoundingClientRect()));
1900
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1901
+ top: 0,
1902
+ left: 0,
1903
+ width: bodyWidth,
1904
+ height: 300 - 10
1905
+ }));
1906
+ expect(records[1].isIntersecting).to.be(true);
1907
+ // (300 - 10) / 402 == ~0.721
1908
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
1909
+
1910
+ // The target1 is clipped at the top by the iframe's clipping.
1911
+ var clientRect1 = rect({
1912
+ top: 0,
1913
+ left: 0,
1914
+ width: bodyWidth,
1915
+ height: 200
1916
+ });
1917
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1918
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
1919
+ expect(records[2].isIntersecting).to.be(true);
1920
+ expect(records[2].intersectionRatio).to.be(1);
1921
+
1922
+ done();
1923
+ io.disconnect();
1924
+ }, {}, parentRect);
1925
+ io.observe(documentElement);
1926
+ io.observe(body);
1927
+ io.observe(iframeTargetEl1);
1928
+ });
1929
+
1930
+ it('calculates rects for a fully visible and scrolled frame', function(done) {
1931
+ iframeWin.scrollTo(0, 10);
1932
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
1933
+ var io = createObserver(function(unsortedRecords) {
1934
+ var records = sortRecords(unsortedRecords);
1935
+ expect(records.length).to.be(3);
1936
+ checkRootBoundsAreNull(records);
1937
+
1938
+ // The documentElement is partially visible.
1939
+ expect(rect(records[0].boundingClientRect))
1940
+ .to.eql(rect(documentElement.getBoundingClientRect()));
1941
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
1942
+ top: 0,
1943
+ left: 0,
1944
+ width: bodyWidth,
1945
+ height: 300
1946
+ }));
1947
+ expect(records[0].isIntersecting).to.be(true);
1948
+ // 300 / 404 == ~0.743
1949
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
1950
+
1951
+ // The document.body is partially visible.
1952
+ expect(rect(records[1].boundingClientRect))
1953
+ .to.eql(rect(body.getBoundingClientRect()));
1954
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
1955
+ top: 0,
1956
+ left: 0,
1957
+ width: bodyWidth,
1958
+ height: 300
1959
+ }));
1960
+ expect(records[1].isIntersecting).to.be(true);
1961
+ // 300 / 402 == ~0.746
1962
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
1963
+
1964
+ // The target1 is fully visible.
1965
+ var clientRect1 = rect({
1966
+ top: -10,
1967
+ left: 0,
1968
+ width: bodyWidth,
1969
+ height: 200
1970
+ });
1971
+ var intersectRect1 = rect({
1972
+ top: 0,
1973
+ left: 0,
1974
+ width: bodyWidth,
1975
+ // Height is only for the visible area.
1976
+ height: 200 - 10
1977
+ });
1978
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
1979
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
1980
+ expect(records[2].isIntersecting).to.be(true);
1981
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
1982
+
1983
+ done();
1984
+ io.disconnect();
1985
+ }, {}, parentRect);
1986
+ io.observe(documentElement);
1987
+ io.observe(body);
1988
+ io.observe(iframeTargetEl1);
1989
+ });
1990
+
1991
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
1992
+ iframeWin.scrollTo(0, 10);
1993
+ var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
1994
+ var io = createObserver(function(unsortedRecords) {
1995
+ var records = sortRecords(unsortedRecords);
1996
+ expect(records.length).to.be(4);
1997
+ checkRootBoundsAreNull(records);
1998
+
1999
+ // The documentElement is partially visible.
2000
+ expect(rect(records[0].boundingClientRect))
2001
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2002
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2003
+ top: 10,
2004
+ left: 0,
2005
+ width: bodyWidth,
2006
+ height: 300 - 10
2007
+ }));
2008
+ expect(records[0].isIntersecting).to.be(true);
2009
+ // (300 - 10) / 404 == ~0.717
2010
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2011
+
2012
+ // The document.body is partially visible.
2013
+ expect(rect(records[1].boundingClientRect))
2014
+ .to.eql(rect(body.getBoundingClientRect()));
2015
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2016
+ top: 10,
2017
+ left: 0,
2018
+ width: bodyWidth,
2019
+ height: 300 - 10
2020
+ }));
2021
+ expect(records[1].isIntersecting).to.be(true);
2022
+ // (300 - 10) / 402 == ~0.721
2023
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2024
+
2025
+ // The target1 is clipped at the top by the iframe's clipping.
2026
+ var clientRect1 = rect({
2027
+ top: -10,
2028
+ left: 0,
2029
+ width: bodyWidth,
2030
+ height: 200
2031
+ });
2032
+ var intersectRect1 = rect({
2033
+ left: 0,
2034
+ width: bodyWidth,
2035
+ // Top is clipped.
2036
+ top: 10,
2037
+ // The height is less by both: offset and scroll.
2038
+ height: 200 - 10 - 10
2039
+ });
2040
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2041
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2042
+ expect(records[2].isIntersecting).to.be(true);
2043
+ expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
2044
+
2045
+ // The target2 is partially visible.
2046
+ var clientRect2 = rect({
2047
+ top: 202 - 10,
2048
+ left: 0,
2049
+ width: bodyWidth,
2050
+ height: 200
2051
+ });
2052
+ var intersectRect2 = rect({
2053
+ top: 202 - 10,
2054
+ left: 0,
2055
+ width: bodyWidth,
2056
+ // The bottom is clipped off.
2057
+ bottom: 300
2058
+ });
2059
+ expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
2060
+ expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
2061
+ expect(records[3].isIntersecting).to.be(true);
2062
+ expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
2063
+
2064
+ done();
2065
+ io.disconnect();
2066
+ }, {}, parentRect);
2067
+ io.observe(documentElement);
2068
+ io.observe(body);
2069
+ io.observe(iframeTargetEl1);
2070
+ io.observe(iframeTargetEl2);
2071
+ });
2072
+
2073
+ it('calculates rects for a fully clipped frame', function(done) {
2074
+ var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
2075
+ var io = createObserver(function(unsortedRecords) {
2076
+ var records = sortRecords(unsortedRecords);
2077
+ expect(records.length).to.be(3);
2078
+ checkRootBoundsAreNull(records);
2079
+
2080
+ var emptyRect = rect({
2081
+ top: 0,
2082
+ left: 0,
2083
+ width: 0,
2084
+ height: 0
2085
+ });
2086
+
2087
+ // The documentElement is completely invisible.
2088
+ expect(rect(records[0].boundingClientRect))
2089
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2090
+ expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
2091
+ expect(records[0].isIntersecting).to.be(false);
2092
+ expect(records[0].intersectionRatio).to.be(0);
2093
+
2094
+ // The document.body is completely invisible.
2095
+ expect(rect(records[1].boundingClientRect))
2096
+ .to.eql(rect(body.getBoundingClientRect()));
2097
+ expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
2098
+ expect(records[1].isIntersecting).to.be(false);
2099
+ expect(records[1].intersectionRatio).to.be(0);
2100
+
2101
+ // The target1 is completely invisible.
2102
+ var clientRect1 = rect({
2103
+ top: 0,
2104
+ left: 0,
2105
+ width: bodyWidth,
2106
+ height: 200
2107
+ });
2108
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2109
+ expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
2110
+ expect(records[2].isIntersecting).to.be(false);
2111
+ expect(records[2].intersectionRatio).to.be(0);
2112
+
2113
+ done();
2114
+ io.disconnect();
2115
+ }, {}, parentRect);
2116
+ io.observe(documentElement);
2117
+ io.observe(body);
2118
+ io.observe(iframeTargetEl1);
2119
+ });
2120
+
2121
+ it('blocks until crossOriginUpdater is called first time', function(done) {
2122
+ if (supportsNativeIntersectionObserver(iframeWin)) {
2123
+ // Skip: not possible to emulate with the native observer.
2124
+ done();
2125
+ return;
2126
+ }
2127
+
2128
+ var spy = sinon.spy();
2129
+
2130
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2131
+
2132
+ var io = createObserver(spy, {});
2133
+ io.observe(iframeTargetEl1);
2134
+
2135
+ runSequence([
2136
+ function(done) {
2137
+ setTimeout(function() {
2138
+ expect(spy.callCount).to.be(0);
2139
+
2140
+ // Issue the first update.
2141
+ crossOriginUpdater(parentRect, null);
2142
+
2143
+ done();
2144
+ }, ASYNC_TIMEOUT);
2145
+ },
2146
+ function(done) {
2147
+ setTimeout(function() {
2148
+ expect(spy.callCount).to.be(1);
2149
+ var records = sortRecords(spy.lastCall.args[0]);
2150
+ expect(records.length).to.be(1);
2151
+ expect(records[0].intersectionRatio).to.be(0);
2152
+ expect(records[0].isIntersecting).to.be(false);
2153
+ done();
2154
+ }, ASYNC_TIMEOUT);
2155
+ },
2156
+ function(done) {
2157
+ io.disconnect();
2158
+ done();
2159
+ }
2160
+ ], done);
2161
+ });
2162
+
2163
+ it('doesn\'t block with a root specified', function(done) {
2164
+ var spy = sinon.spy();
2165
+
2166
+ var io = createObserver(spy, {root: body});
2167
+ io.observe(iframeTargetEl1);
2168
+
2169
+ runSequence([
2170
+ function(done) {
2171
+ setTimeout(function() {
2172
+ expect(spy.callCount).to.be(1);
2173
+ var record = sortRecords(spy.lastCall.args[0])[0];
2174
+ expect(record.intersectionRatio).to.be(1);
2175
+ expect(record.isIntersecting).to.be(true);
2176
+ expect(rect(record.rootBounds)).to.eql(rect(body.getBoundingClientRect()));
2177
+ done();
2178
+ }, ASYNC_TIMEOUT);
2179
+ },
2180
+ function(done) {
2181
+ io.disconnect();
2182
+ done();
2183
+ }
2184
+ ], done);
2185
+ });
2186
+
2187
+ it('handles style changes', function(done) {
2188
+ var spy = sinon.spy();
2189
+
2190
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2191
+
2192
+ // When first element becomes invisible, the second element will show.
2193
+ // And in reverse: when the first element becomes visible again, the
2194
+ // second element will disappear.
2195
+ var io = createObserver(spy, {}, parentRect);
2196
+ io.observe(iframeTargetEl1);
2197
+ io.observe(iframeTargetEl2);
2198
+
2199
+ runSequence([
2200
+ function(done) {
2201
+ setTimeout(function() {
2202
+ expect(spy.callCount).to.be(1);
2203
+ var records = sortRecords(spy.lastCall.args[0]);
2204
+ expect(records.length).to.be(2);
2205
+ expect(records[0].intersectionRatio).to.be(1);
2206
+ expect(records[0].isIntersecting).to.be(true);
2207
+ expect(records[1].intersectionRatio).to.be(0);
2208
+ expect(records[1].isIntersecting).to.be(false);
2209
+ done();
2210
+ }, ASYNC_TIMEOUT);
2211
+ },
2212
+ function(done) {
2213
+ iframeTargetEl1.style.display = 'none';
2214
+ setTimeout(function() {
2215
+ expect(spy.callCount).to.be(2);
2216
+ var records = sortRecords(spy.lastCall.args[0]);
2217
+ expect(records.length).to.be(2);
2218
+ expect(records[0].intersectionRatio).to.be(0);
2219
+ expect(records[0].isIntersecting).to.be(false);
2220
+ expect(records[1].intersectionRatio).to.be(1);
2221
+ expect(records[1].isIntersecting).to.be(true);
2222
+ done();
2223
+ }, ASYNC_TIMEOUT);
2224
+ },
2225
+ function(done) {
2226
+ iframeTargetEl1.style.display = '';
2227
+ setTimeout(function() {
2228
+ expect(spy.callCount).to.be(3);
2229
+ var records = sortRecords(spy.lastCall.args[0]);
2230
+ expect(records.length).to.be(2);
2231
+ expect(records[0].intersectionRatio).to.be(1);
2232
+ expect(records[0].isIntersecting).to.be(true);
2233
+ expect(records[1].intersectionRatio).to.be(0);
2234
+ expect(records[1].isIntersecting).to.be(false);
2235
+ done();
2236
+ }, ASYNC_TIMEOUT);
2237
+ },
2238
+ function(done) {
2239
+ io.disconnect();
2240
+ done();
2241
+ }
2242
+ ], done);
2243
+ });
2244
+
2245
+ it('handles scroll changes', function(done) {
2246
+ var spy = sinon.spy();
2247
+
2248
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2249
+
2250
+ // Scrolling to the middle of the iframe shows the second box and
2251
+ // hides the first.
2252
+ var io = createObserver(spy, {}, parentRect);
2253
+ io.observe(iframeTargetEl1);
2254
+ io.observe(iframeTargetEl2);
2255
+
2256
+ runSequence([
2257
+ function(done) {
2258
+ setTimeout(function() {
2259
+ expect(spy.callCount).to.be(1);
2260
+ var records = sortRecords(spy.lastCall.args[0]);
2261
+ expect(records.length).to.be(2);
2262
+ expect(records[0].intersectionRatio).to.be(1);
2263
+ expect(records[0].isIntersecting).to.be(true);
2264
+ expect(records[1].intersectionRatio).to.be(0);
2265
+ expect(records[1].isIntersecting).to.be(false);
2266
+ done();
2267
+ }, ASYNC_TIMEOUT);
2268
+ },
2269
+ function(done) {
2270
+ iframeWin.scrollTo(0, 202);
2271
+ setTimeout(function() {
2272
+ expect(spy.callCount).to.be(2);
2273
+ var records = sortRecords(spy.lastCall.args[0]);
2274
+ expect(records.length).to.be(2);
2275
+ expect(records[0].intersectionRatio).to.be(0);
2276
+ expect(records[0].isIntersecting).to.be(false);
2277
+ expect(records[1].intersectionRatio).to.be(1);
2278
+ expect(records[1].isIntersecting).to.be(true);
2279
+ done();
2280
+ }, ASYNC_TIMEOUT);
2281
+ },
2282
+ function(done) {
2283
+ iframeWin.scrollTo(0, 0);
2284
+ setTimeout(function() {
2285
+ expect(spy.callCount).to.be(3);
2286
+ var records = sortRecords(spy.lastCall.args[0]);
2287
+ expect(records.length).to.be(2);
2288
+ expect(records[0].intersectionRatio).to.be(1);
2289
+ expect(records[0].isIntersecting).to.be(true);
2290
+ expect(records[1].intersectionRatio).to.be(0);
2291
+ expect(records[1].isIntersecting).to.be(false);
2292
+ done();
2293
+ }, ASYNC_TIMEOUT);
2294
+ },
2295
+ function(done) {
2296
+ io.disconnect();
2297
+ done();
2298
+ }
2299
+ ], done);
2300
+ });
2301
+
2302
+ it('handles parent rect changes', function(done) {
2303
+ var spy = sinon.spy();
2304
+
2305
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2306
+
2307
+ // Iframe goes off screen and returns.
2308
+ var io = createObserver(spy, {}, parentRect);
2309
+ io.observe(iframeTargetEl1);
2310
+ io.observe(iframeTargetEl2);
2311
+
2312
+ runSequence([
2313
+ function(done) {
2314
+ setTimeout(function() {
2315
+ expect(spy.callCount).to.be(1);
2316
+ var records = sortRecords(spy.lastCall.args[0]);
2317
+ expect(records.length).to.be(2);
2318
+ checkRootBoundsAreNull(records);
2319
+ expect(records[0].intersectionRatio).to.be(1);
2320
+ expect(records[0].isIntersecting).to.be(true);
2321
+ expect(records[1].intersectionRatio).to.be(0);
2322
+ expect(records[1].isIntersecting).to.be(false);
2323
+ // Top-level bounds.
2324
+ expect(records[0].intersectionRect.height).to.be(200);
2325
+ done();
2326
+ }, ASYNC_TIMEOUT);
2327
+ },
2328
+ function(done) {
2329
+ // Completely off screen.
2330
+ applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
2331
+ setTimeout(function() {
2332
+ expect(spy.callCount).to.be(2);
2333
+ var records = sortRecords(spy.lastCall.args[0]);
2334
+ expect(records.length).to.be(1);
2335
+ checkRootBoundsAreNull(records);
2336
+ expect(records[0].intersectionRatio).to.be(0);
2337
+ expect(records[0].isIntersecting).to.be(false);
2338
+ // Top-level bounds.
2339
+ expect(records[0].intersectionRect.height).to.be(0);
2340
+ done();
2341
+ }, ASYNC_TIMEOUT);
2342
+ },
2343
+ function(done) {
2344
+ // Partially returns.
2345
+ applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
2346
+ setTimeout(function() {
2347
+ expect(spy.callCount).to.be(3);
2348
+ var records = sortRecords(spy.lastCall.args[0]);
2349
+ expect(records.length).to.be(1);
2350
+ checkRootBoundsAreNull(records);
2351
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
2352
+ expect(records[0].isIntersecting).to.be(true);
2353
+ // Top-level bounds.
2354
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
2355
+ done();
2356
+ }, ASYNC_TIMEOUT);
2357
+ },
2358
+ function(done) {
2359
+ io.disconnect();
2360
+ done();
2361
+ }
2362
+ ], done);
2363
+ });
2364
+ });
2365
+ });
944
2366
  });
945
2367
 
946
2368
 
@@ -967,11 +2389,13 @@ function runSequence(functions, done) {
967
2389
  /**
968
2390
  * Returns whether or not the current browser has native support for
969
2391
  * IntersectionObserver.
2392
+ * @param {Window=} win
970
2393
  * @return {boolean} True if native support is detected.
971
2394
  */
972
- function supportsNativeIntersectionObserver() {
973
- return 'IntersectionObserver' in window &&
974
- window.IntersectionObserver.toString().indexOf('[native code]') > -1;
2395
+ function supportsNativeIntersectionObserver(win) {
2396
+ win = win || window;
2397
+ return 'IntersectionObserver' in win &&
2398
+ win.IntersectionObserver.toString().indexOf('[native code]') > -1;
975
2399
  }
976
2400
 
977
2401