intersection-observer 0.6.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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