arrive
Version:
arrive.js provides events to watch for DOM elements creation and removal. It makes use of Mutation Observers internally.
582 lines (465 loc) • 22.8 kB
JavaScript
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
describe("Arrive", function() {
beforeEach(function() {
// clear all binded events before running each test case
Arrive.unbindAllArrive();
Arrive.unbindAllLeave();
});
describe("Arrive Event Tests", function() {
describe("Binding events to different element types:", function() {
var selector = ".test-elem";
it("event should be fired when 'arrive' event is binded to window", function(done) {
var $appendedElem = $("<div class='test-elem'></div>");
j(window).arrive(selector, function() {
expect(this).toBe($appendedElem[0]);
done();
});
$("body").append($appendedElem);
});
});
describe("Selector involving single element:", function() {
var selector = ".test-elem";
it("event should be fired when element with specified class is injected to DOM", function(done) {
var $appendedElem = $("<div class='test-elem'></div>");
j(document).arrive(selector, function() {
expect(this).toBe($appendedElem[0]);
done();
});
$("body").append($appendedElem);
});
});
describe("Selector involving nested elements: div.container1 .container2 .btn.red", function() {
var selector = "div.container1 .container2 .btn.red";
$("body").append("<div class='container1'></div>");
it("event should be fired when a tree is inserted and it contains an element which satisfy the selector", function(done) {
var $appendedElem = $("<div class='container2'><span class='btn red'></span></div>"),
$redBtn = $appendedElem.find(".btn.red");
j(document).arrive(selector, function() {
expect(this).toBe($redBtn[0]);
done();
});
$("body .container1").append($appendedElem);
});
it("event should be fired when target element is directly injected in DOM", function(done) {
$("body .container1").children().remove();
var $redBtn = $("<span class='btn red'></span>");
j(document).arrive(selector, function() {
expect(this).toBe($redBtn[0]);
done();
});
$("body .container1").append($("<div class='container2'>"));
$("body .container1 .container2").append($redBtn);
});
});
describe("Arrive event on attribute modification of an element.", function() {
var $elem = $("<div class='container5'><div class='btn'></div></div>"),
$btn = $elem.find(".btn");
$("body").append($elem);
it("Event should be fired when a class is added to an element and the element starts to satisfies event selector", function(done) {
j(document).arrive(".container5 .btn.red", { fireOnAttributesModification: true }, function() {
expect(this).toBe($btn[0]);
done();
});
$btn.addClass("red");
});
it("Event should be fired when tooltip is added to an element and the element starts to satisfies event selector", function(done) {
j(document).arrive(".container5 .btn[title='it works!']", { fireOnAttributesModification: true } , function() {
expect(this).toBe($btn[0]);
done();
});
$btn.attr("title", "it works!");
});
it("Event should be not fired when a class is added to an element and the element starts to satisfies event selector but fireOnAttributesModification option is false", function(done) {
var eventFired = false;
j(document).arrive(".container5 .btn.red", function() {
eventFired = true;
});
$btn.addClass("red");
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
it("Event should be not fired when tooltip is added to an element and the element starts to satisfies event selector but fireOnAttributesModification option is false", function(done) {
var eventFired = false;
j(document).arrive(".container5 .btn[title='it works!']", function() {
eventFired = true;
});
$btn.attr("title", "it works!");
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
});
describe("Event unbinding tests", function() {
var eventFired,
selector = ".test-elem",
callback = function() {
eventFired = true;
};
beforeEach(function() {
eventFired = false;
j(document).arrive(selector, callback);
});
it("arrive event should not be fired when unbind is called", function(done) {
j(document).unbindArrive();
$("body").append($("<div class='test-elem'></div>"));
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
it("arrive event should not be fired when unbind is called with selector as an argument", function(done) {
j(document).unbindArrive(selector);
$("body").append($("<div class='test-elem'></div>"));
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
it("arrive event should not be fired when unbind is called with callback as an argument", function(done) {
j(document).unbindArrive(callback);
$("body").append($("<div class='test-elem'></div>"));
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
it("arrive event should not be fired when unbind is called with selector and callback as arguments", function(done) {
j(document).unbindArrive(selector, callback);
$("body").append($("<div class='test-elem'></div>"));
setTimeout(function() {
expect(eventFired).not.toBeTruthy();
done();
}, 400);
});
it("arrive event should not be fired once unbind is called from within arrive callback", function(done) {
var callbackCount = 0;
j(document).arrive(".some-class", { existing: true }, function () {
document.unbindArrive(".some-class");
callbackCount++;
});
$("body").append($("<div class='some-class'></div>"));
$("body").append($("<div class='some-class'></div>"));
setTimeout(function() {
expect(callbackCount).toBe(1);
done();
}, 400);
});
});
describe("Multiple events tests.", function() {
var selector = ".test-elem",
appendedElem = "<div class='test-elem'></div>";
it("Callback should be called multiple times when multiple elements are injected", function(done) {
var callCount = 0;
j(document).arrive(selector, function() {
callCount += 1;
if (callCount >= 2) {
expect(true).toBe(true);
done();
}
});
$("body").append(appendedElem);
$("body").append(appendedElem);
});
it("onceOnly argument should result in callback being called only once", function(done) {
var callCount = 0;
j(document).arrive(selector, {onceOnly: true}, function() {
callCount += 1;
});
$("body").append(appendedElem);
$("body").append(appendedElem);
setTimeout(function() {
expect(callCount).toBe(1);
done();
}, 400);
});
});
describe("options.timeout", function() {
// Add beforeEach to clean up
beforeEach(function() {
$(".test-timeout-1, .test-timeout-2, .test-timeout-3, .test-timeout-4").remove();
});
afterEach(function() {
// Clean up any remaining event listeners
Arrive.unbindAllArrive();
});
it("should call callback with null when element is not found within timeout", function(done) {
var selector = ".test-timeout-1";
j(document).arrive(selector, { timeout: 200 }, function(elem) {
expect(elem).toBe(null);
done();
});
});
it("should call callback with element when found before timeout", function(done) {
var selector = ".test-timeout-2";
var $appendedElem = $("<div class='test-timeout-2'></div>");
j(document).arrive(selector, { timeout: 200 }, function(elem) {
expect(elem).toBe($appendedElem[0]);
done();
});
setTimeout(function() {
$("body").append($appendedElem);
}, 100);
});
it("should call callback after timeout when onceOnly is true", function(done) {
var selector = ".test-timeout-3";
j(document).arrive(selector, { timeout: 200, onceOnly: true }, function(elem) {
expect(elem).toBe(null);
setTimeout(done, 200);
});
setTimeout(function() {
$("body").append($("<div class='test-timeout-3'></div>"));
}, 300);
});
it("should not call callback if arrive has been unbinded", function(done) {
var selector = ".test-timeout-4";
var callCount = 0;
var $elem = $("<div class='test-timeout-4'></div>");
// Set up arrive handler
j(document).arrive(selector, { timeout: 200 }, function(elem) {
callCount++;
});
// Unbind arrive before timeout
setTimeout(function() {
Arrive.unbindAllArrive();
$("body").append($elem);
}, 100);
// Check after timeout that callback was not called
setTimeout(function() {
expect(callCount).toBe(0);
done();
}, 300);
});
it("should restart timeout when element is found and onceOnly is false", function(done) {
var selector = ".test-timeout-4";
var callCount = 0;
var $firstElem = $("<div class='test-timeout-4'></div>");
j(document).arrive(selector, { timeout: 300, onceOnly: false }, function(elem) {
callCount++;
});
// Add first element after 100ms
setTimeout(function() {
$("body").append($firstElem);
}, 100);
// Check after 350ms (after timeout)
setTimeout(function() {
expect(callCount).toBe(1);
}, 350);
// Check after 500ms (after timeout)
setTimeout(function() {
expect(callCount).toBe(2);
done();
}, 500);
});
it("should automatically unbind arrive event after timeout", function(done) {
var selector = ".test-timeout-5";
var callCount = 0;
j(document).arrive(selector, { timeout: 200 }, function(elem) {
callCount++;
});
// Wait for timeout to pass
setTimeout(function() {
// Add element after timeout
$("body").append($("<div class='test-timeout-5'></div>"));
// Check after a delay that callback wasn't called
setTimeout(function() {
expect(callCount).toBe(1); // Should only be called once with null
done();
}, 200);
}, 300);
});
});
describe("options.existing", function() {
var selector = ".test-existing";
beforeEach(function() {
$(".test-existing").remove();
});
it("callback should be called for existing element if options.existing is true", function(done) {
var $existingElementA = $("<div class='" + selector.substring(1) + "'></div>");
var $existingElementB = $existingElementA.clone();
$("body").append($existingElementA).append($existingElementB);
var count = 0;
j(document).arrive(selector, { existing: true }, function() {
expect(this).toBe(!count ? $existingElementA[0] : $existingElementB[0]);
if ((count += 1) === 2) {
done();
}
});
});
it("callback should be called for one existing element only if options.existing and options.onceOnly both are true", function(done) {
var $existingElementA = $("<div class='" + selector.substring(1) + "'></div>");
var $existingElementB = $existingElementA.clone();
$("body").append($existingElementA).append($existingElementB);
j(document).arrive(selector, { existing: true, onceOnly: true }, function() {
expect(this).toBe($existingElementA[0]);
done();
});
});
});
describe("Async/await and promise support:", function() {
var selector = ".test-elem";
beforeEach(function() {
$(selector).remove();
});
it("should return the appended element", async function(done) {
var $elemToAppend = $("<div class='test-elem'></div>");
setTimeout(() => {$("body").append($elemToAppend);}, 250);
var arrivedElem = await j(document).arrive(selector);
expect(arrivedElem).toBe($elemToAppend[0]);
done();
});
it("should return the existing element", async function(done) {
var $existingElement = $("<div class='test-elem'></div>")[0];
$("body").append($existingElement);
var arrivedElem = await j(document).arrive(selector, {existing: true});
expect(arrivedElem).toBe($existingElement);
done();
});
});
});
describe("Leave Event Tests", function() {
var selector = ".test-elem";
it("event should be fired when element with specified class is removed from DOM", function(done) {
$(selector).remove(); // remove any previous test element in DOM
var $toBeRemoved = $("<div><div class='test-elem'></div></div>"),
$testElem = $toBeRemoved.find(".test-elem");
$("body").append($toBeRemoved);
j(document).leave(selector, function() {
expect(this).toBe($testElem[0]);
done();
});
$(selector).remove();
});
describe("Selector involving nested elements: div.container1 .container2 .btn.red", function() {
var selector = ".btn.red",
$redBtn = null;
beforeEach(function() {
$(".container1,.container5").remove();
var $container1 = $("<div class='container1'><div class='container2'><span class='btn red'></span></div></div>");
$redBtn = $container1.find(".btn.red");
$("body").append($container1);
});
it("event should be fired when a tree is removed and it contains an element which satisfy the selector", function(done) {
j(document).leave(selector, function() {
expect(this).toBe($redBtn[0]);
done();
});
$(".container2").remove();
});
it("event should be fired when target element is directly removed from DOM", function(done) {
j(document).leave(selector, function() {
expect(this).toBe($redBtn[0]);
done();
});
$(".btn.red").remove();
});
});
describe("Calling arrive function on NodeList and HTMLElement", function() {
it("arrive function should be callable on NodeList", function() {
document.getElementsByTagName("body").arrive(".test", function() {});
expect(true).toBeTruthy();
});
it("arrive function should be callable on HTMLElement", function() {
document.getElementsByTagName("body")[0].arrive(".test", function() {});
expect(true).toBeTruthy();
});
});
describe("Async/await and promise support:", function() {
var selector = ".test-elem";
beforeEach(function() {
$(selector).remove();
var $elemToBeRemoved = $("<div class='test-elem'></div>");
$("body").append($elemToBeRemoved);
});
it("should finish once the existing element is removed", async function(done) {
var $elemThatWillBeRemoved = $(selector)[0];
setTimeout(() => {$(selector).remove();}, 250);
var $removedEle = await j(document).leave(selector);
expect($removedEle).toBe($elemThatWillBeRemoved);
done();
});
});
describe("options.timeout", function() {
beforeEach(function() {
$(".test-timeout-leave-1, .test-timeout-leave-2").remove();
});
afterEach(function() {
Arrive.unbindAllLeave();
});
it("should call callback with null when element is not removed within timeout", function(done) {
var selector = ".test-timeout-leave-1";
var $elem = $("<div class='test-timeout-leave-1'></div>");
$("body").append($elem);
j(document).leave(selector, { timeout: 200 }, function(elem) {
expect(elem).toBe(null);
done();
});
});
it("should call callback with element when removed before timeout", function(done) {
var selector = ".test-timeout-leave-2";
var $elem = $("<div class='test-timeout-leave-2'></div>");
$("body").append($elem);
j(document).leave(selector, { timeout: 200 }, function(elem) {
expect(elem).toBe($elem[0]);
done();
});
setTimeout(function() {
$elem.remove();
}, 100);
});
});
describe("Multiple leave events tests", function() {
var selector = ".test-leave-multiple";
beforeEach(function() {
Arrive.unbindAllLeave();
$(selector).remove();
});
it("Callback should be called multiple times when multiple elements are removed", function(done) {
var callCount = 0;
var $elements = $("<div class='test-leave-multiple'></div><div class='test-leave-multiple'></div>");
$("body").append($elements);
j(document).leave(selector, function() {
callCount += 1;
if (callCount >= 2) {
expect(callCount).toBe(2);
done();
}
});
$(selector).remove();
});
it("onceOnly option should result in callback being called only once", function(done) {
var callCount = 0;
var $elements = $("<div class='test-leave-multiple'></div><div class='test-leave-multiple'></div>");
$("body").append($elements);
j(document).leave(selector, { onceOnly: true }, function() {
callCount += 1;
});
$(selector).remove();
setTimeout(function() {
expect(callCount).toBe(1);
done();
}, 400);
});
});
});
describe("ES2015 arrow function support", function() {
var selector = ".test-elem";
it("Make sure the first argument equals `this` object", function(done) {
var $appendedElem = $("<div class='test-elem'></div>");
j(document).arrive(selector, function(elem) {
expect(this).toBe(elem);
done();
});
$("body").append($appendedElem);
});
it("Make sure the first argument equals `this` object with `options.onceOnly` and `options.existing`", function(done) {
j(document).arrive(selector, {onceOnly: true, existing: true}, function(elem) {
expect(this).toBe(elem);
done();
$(selector).remove();
});
});
});
});