define([
'intern!object',
'intern/chai!assert',
'../../on',
'../../Evented',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/has',
'dojo/has!host-browser?dojo/dom-construct',
// Included to test on.selector
'dojo/has!host-browser?../../query',
'dojo/has!host-browser?dojo/domReady!'
], function (registerSuite, assert, on, Evented, lang, arrayUtil, has, domConstruct) {
var handles = [];
var originalOn = on;
on = function () {
var handle = originalOn.apply(null, arguments);
handles.push(handle);
return handle;
};
for (var key in originalOn) {
on[key] = originalOn[key];
}
function cleanUpListeners() {
while (handles.length > 0) {
handles.pop().remove();
}
}
function createCommonTests(args) {
var target,
testEventName = args.eventName;
return {
beforeEach: function () {
target = args.createTarget();
},
afterEach: function () {
// This would ideally be specified in a suite-wide afterEach,
// but Safari throws exceptions if listener clean-up occurs after DOM nodes are destroyed
cleanUpListeners();
args.destroyTarget && args.destroyTarget(target);
},
'on and on.emit': function () {
var listenerCallCount = 0,
emittedEvent;
on(target, testEventName, function (actualEvent) {
listenerCallCount++;
assert.strictEqual(actualEvent.value, emittedEvent.value);
});
emittedEvent = { value: 'foo' };
on.emit(target, testEventName, emittedEvent);
assert.strictEqual(listenerCallCount, 1);
emittedEvent = { value: 'bar' };
on.emit(target, testEventName, emittedEvent);
assert.strictEqual(listenerCallCount, 2);
},
'.emit return value': function () {
// NOTE: Uncancelable events are treated inconsistently.
// The return value depends on whether the browser DOM offers
// an `addEventListener` method and whether the event is a DOM or simple object event.
// Therefore, the purpose of testing the return value for uncancelable events
// is to codify current behavior and catch unintentional changes.
var returnValue = on.emit(target, testEventName, { cancelable: false });
if (has('dom-addeventlistener') && 'dispatchEvent' in target) {
assert.ok(returnValue);
assert.propertyVal(returnValue, 'cancelable', false);
}
else {
assert.isFalse(returnValue);
}
returnValue = on.emit(target, testEventName, { cancelable: true });
assert.ok(returnValue);
assert.propertyVal(returnValue, 'cancelable', true);
on(target, testEventName, function (event) {
if ('preventDefault' in event) {
event.preventDefault();
}
else {
event.cancelable = false;
}
});
assert.isFalse(on.emit(target, testEventName, { cancelable: true }));
},
'on - multiple event names': function () {
var listenerCallCount = 0,
emittedEventType,
emittedEvent;
on(target, 'test1, test2', function (actualEvent) {
listenerCallCount++;
if (emittedEventType in actualEvent) {
assert.strictEqual(actualEvent.type, emittedEventType);
}
assert.strictEqual(actualEvent.value, emittedEvent.value);
});
emittedEventType = 'test1';
emittedEvent = { value: 'foo' };
on.emit(target, emittedEventType, emittedEvent);
assert.strictEqual(listenerCallCount, 1);
emittedEventType = 'test2';
emittedEvent = { value: 'bar' };
on.emit(target, emittedEventType, emittedEvent);
assert.strictEqual(listenerCallCount, 2);
},
'on - multiple handlers': function () {
var order = [];
var customEvent = function (target, listener) {
return on(target, 'custom', listener);
};
on(target, 'a, b', function (event) {
order.push(1 + event.type);
});
on(target, [ 'a', customEvent ], function (event) {
order.push(2 + event.type);
});
on.emit(target, 'a', { type: 'a' });
on.emit(target, 'b', { type: 'b' });
on.emit(target, 'custom', { type: 'custom' });
assert.deepEqual(order, [ '1a', '2a', '1b', '2custom' ]);
},
'on - extension events': function () {
var listenerCallCount = 0,
emittedEvent,
extensionEvent = function (target, listener) {
return on(target, testEventName, listener);
};
on(target, extensionEvent, function (actualEvent) {
listenerCallCount++;
assert.strictEqual(actualEvent.value, emittedEvent.value);
});
emittedEvent = { value: 'foo' };
on.emit(target, testEventName, emittedEvent);
assert.strictEqual(listenerCallCount, 1);
emittedEvent = { value: 'bar' };
on.emit(target, testEventName, emittedEvent);
assert.strictEqual(listenerCallCount, 2);
},
'.pausable': function () {
var listenerCallCount = 0,
handle = on.pausable(target, testEventName, function () {
listenerCallCount++;
});
on.emit(target, testEventName, {});
assert.strictEqual(listenerCallCount, 1);
handle.pause();
on.emit(target, testEventName, {});
assert.strictEqual(listenerCallCount, 1);
handle.resume();
on.emit(target, testEventName, {});
assert.strictEqual(listenerCallCount, 2);
},
'.once': function () {
var listenerCallCount = 0;
on.once(target, testEventName, function () {
++listenerCallCount;
});
assert.strictEqual(listenerCallCount, 0);
on.emit(target, testEventName, {});
assert.strictEqual(listenerCallCount, 1);
on.emit(target, testEventName, {});
assert.strictEqual(listenerCallCount, 1);
},
'hitch no selector': function () {
var div = document.body.appendChild(document.createElement("div"));
var span = div.appendChild(document.createElement("span"));
var button = span.appendChild(document.createElement("button"));
var that = { fake: true };
var result = {
contextIsOk: false,
selectorTargetIsOk: false
}
on(div, "click", lang.hitch(that, function (e) {
result.contextIsOk = this === that;
result.selectorTargetIsOk = e.selectorTarget === undefined; //selectorTarget is only available with event delegation
}));
on.emit(button, 'click', { cancelable: true, bubbles: true });
assert.isTrue(result.contextIsOk, "contextIsOk");
assert.isTrue(result.selectorTargetIsOk, "selectorTargetIsOk");
},
'hitch with selector': function () {
var div = document.body.appendChild(document.createElement("div"));
var span = div.appendChild(document.createElement("span"));
var button = span.appendChild(document.createElement("button"));
var that = { fake: true };
var result = {
contextIsOk: false,
selectorTargetIsOk: false
}
on(div, "span:click", lang.hitch(that, function (e) {
result.contextIsOk = this === that;
result.selectorTargetIsOk = e.selectorTarget === span;
}));
on.emit(button, 'click', { cancelable: true, bubbles: true });
assert.isTrue(result.contextIsOk, "contextIsOk");
assert.isTrue(result.selectorTargetIsOk, "selectorTargetIsOk");
},
'listener call order': function () {
var order = [],
onMethodName = 'on' + testEventName;
target[onMethodName] = function (event) {
order.push(event.a);
};
var signal = on.pausable(target, testEventName, function () {
order.push(1);
});
var signal2 = on(target, testEventName + ', foo', function (event) {
order.push(event.a);
});
on.emit(target, testEventName, {
a: 3
});
signal.pause();
var signal3 = on(target, testEventName, function () {
order.push(3);
}, true);
on.emit(target, testEventName, {
a: 3
});
signal2.remove();
signal.resume();
on.emit(target, testEventName, {
a: 6
});
signal3.remove();
on(target, 'foo, ' + testEventName, function () {
order.push(4);
}, true);
signal.remove();
on.emit(target, testEventName, {
a: 7
});
assert.deepEqual(order, [ 3, 1, 3, 3, 3, 3, 6, 1, 3, 7, 4 ]);
}
};
}
var suite = {
name: 'dojo/on',
common: {
'object events': createCommonTests({
eventName: 'test',
createTarget: function () {
return new Evented();
}
})
},
'cannot target non-emitter': function () {
var threwError = false;
try {
var nonEmitter = {};
on(nonEmitter, 'test', function () {});
}
catch (err) {
threwError = true;
}
assert.isTrue(threwError);
}
};
if (has('host-browser')) {
suite.common['DOM events'] = createCommonTests({
eventName: 'click',
createTarget: function () {
return domConstruct.create('div', null, document.body);
},
destroyTarget: function (target) {
domConstruct.destroy(target);
}
});
// TODO: Add test to cover syntheticStopPropagation
// TODO: Add tests to cover functionality of _fixEvent
var containerDiv,
childSpan;
suite['DOM-specific'] = {
'beforeEach': function () {
containerDiv = domConstruct.create('div', null, document.body);
childSpan = domConstruct.create('span', null, containerDiv);
},
'afterEach': function () {
cleanUpListeners();
domConstruct.destroy(containerDiv);
containerDiv = childSpan = null;
},
'event.preventDefault': {
'native event': function () {
var defaultPrevented = false;
on(childSpan, 'click', function (event) {
event.preventDefault();
defaultPrevented = event.defaultPrevented;
});
childSpan.click();
assert.isTrue(defaultPrevented);
},
'synthetic event': function () {
var secondListenerCalled = false,
defaultPrevented = false;
on(childSpan, 'click', function (event) {
event.preventDefault();
});
on(containerDiv, 'click', function (event) {
secondListenerCalled = true;
defaultPrevented = event.defaultPrevented;
});
on.emit(childSpan, 'click', {bubbles: true, cancelable: true});
assert.isTrue(secondListenerCalled, 'bubbled synthetic event on div');
assert.isTrue(defaultPrevented, 'defaultPrevented set for synthetic event on div');
}
},
'event bubbling': function () {
var eventBubbled = false;
on(containerDiv, 'click', function () {
eventBubbled = true;
});
childSpan.click();
assert.isTrue(eventBubbled, 'expected event to bubble');
},
'event.stopPropagation': function () {
var eventBubbled;
on(containerDiv, 'click', function () {
eventBubbled = true;
});
on(childSpan, 'click', function (event) {
event.stopPropagation();
});
// Testing with both Element#click and on.emit because they exercise different
// code paths, most notably with the browsers that require synthetic dispatch and stopPropagation
eventBubbled = false;
childSpan.click();
assert.isFalse(eventBubbled);
eventBubbled = false;
on.emit(childSpan, 'click', {});
assert.isFalse(eventBubbled);
},
'event.stopImmediatePropagation': function () {
on(childSpan, 'click', function (event) {
event.stopImmediatePropagation();
});
var afterStop = false;
on(childSpan, 'click', function () {
afterStop = true;
});
childSpan.click();
assert.isFalse(afterStop, 'expected no other listener to be called');
},
'emitting events from document and window': function () {
// make sure 'document' and 'window' can also emit events
var eventEmitted;
var iframe = domConstruct.place('', containerDiv);
var globalObjects = [
document, window, iframe.contentWindow, iframe.contentDocument || iframe.contentWindow.document
];
for (var i = 0, len = globalObjects.length; i < len; i++) {
eventEmitted = false;
on(globalObjects[i], 'custom-test-event', function () {
eventEmitted = true;
});
on.emit(globalObjects[i], 'custom-test-event', {});
assert.isTrue(eventEmitted);
}
},
'event delegation': {
'CSS selector': function () {
var button = domConstruct.create('button', null, childSpan);
var listenerCalled = false;
on(containerDiv, 'button:click', function () {
listenerCalled = true;
});
button.click();
assert.isTrue(listenerCalled);
},
'listening on document': function () {
var button = domConstruct.create('button', null, childSpan);
var listenerCalled = false;
on(document, 'button:click', function () {
listenerCalled = true;
});
button.click();
assert.isTrue(listenerCalled);
},
'CSS selector and text node target': function () {
childSpan.className = 'textnode-parent';
childSpan.innerHTML = 'text';
var listenerCalled;
on(containerDiv, '.textnode-parent:click', function () {
listenerCalled = true;
});
on.emit(childSpan.firstChild, 'click', { bubbles: true, cancelable: true });
assert.isTrue(listenerCalled);
},
'custom selector': function () {
var button = domConstruct.create('button', null, childSpan);
var listenerCalled = false;
on(containerDiv, on.selector(function (node) {
return node.tagName === 'BUTTON';
}, 'click'), function () {
listenerCalled = true;
});
button.click();
assert.isTrue(listenerCalled);
},
'on.selector and extension events': {
'basic extension events': function () {
childSpan.setAttribute('foo', 2);
var order = [];
var customEvent = function (node, listener) {
return on(node, 'custom', listener);
};
on(containerDiv, customEvent, function (event) {
order.push(event.a);
});
on(containerDiv, on.selector('span', customEvent), function () {
order.push(+this.getAttribute('foo'));
});
on.emit(containerDiv, 'custom', {
a: 0
});
// should trigger selector
on.emit(childSpan, 'custom', {
a: 1,
bubbles: true,
cancelable: true
});
// shouldn't trigger selector
on.emit(containerDiv, 'custom', {
a: 3,
bubbles: true,
cancelable: true
});
assert.deepEqual(order, [0, 1, 2, 3]);
},
'extension events with bubbling forms': function () {
var listenerCalled = false,
bubbleListenerCalled = false;
var customEvent = function (node, listener) {
return on(node, 'custom', listener);
};
// simply test that an extension event's bubble method is applied if it exists
customEvent.bubble = function (select) {
return function (node, listener) {
return customEvent(node, function (event) {
bubbleListenerCalled = true;
if (select(event.target)) {
listener(event);
}
});
};
};
on(containerDiv, on.selector('span', customEvent), function () {
listenerCalled = true;
});
on.emit(childSpan, 'custom', { bubbles: true });
assert.isTrue(listenerCalled);
assert.isTrue(bubbleListenerCalled);
}
},
'only call listener when matching': function () {
containerDiv.innerHTML = '';
on(containerDiv, '.matchesNothing:click', function (event) {
event.preventDefault();
});
containerDiv.firstChild.click();
assert.isTrue(containerDiv.firstChild.checked);
}
},
'event augmentation': function () {
var button = domConstruct.create('button', null, containerDiv);
on(button, 'click', function (event) {
event.modified = true;
event.test = 3;
});
var testValue;
on(containerDiv, 'click', function (event) {
testValue = event.test;
});
button.click();
assert.strictEqual(testValue, 3);
}
};
suite['.matches'] = (function () {
var containerDiv2;
return {
beforeEach: function () {
containerDiv = domConstruct.create('div', null, document.body);
containerDiv2 = domConstruct.create('div', null, containerDiv);
childSpan = domConstruct.create('span', null, containerDiv2);
},
afterEach: function () {
cleanUpListeners();
domConstruct.destroy(containerDiv);
containerDiv = containerDiv2 = childSpan = null;
},
'inner-most child click': function () {
on(containerDiv, 'click', function (event) {
assert.ok(on.matches(event.target, 'span:click', this));
assert.ok(on.matches(event.target, 'div:click', this));
assert.ok(!on.matches(event.target, 'div:click', this, false));
assert.ok(!on.matches(event.target, 'body:click', this));
});
childSpan.click();
},
'inner-most container click': function () {
on(containerDiv, 'click', function (event) {
assert.ok(!on.matches(event.target, 'span:click', this));
assert.ok(on.matches(event.target, 'div:click', this));
});
containerDiv2.click();
}
};
})();
// TODO: Add tests to improve touch-related code coverage
has('touch') && (suite['DOM-specific']['touch event normalization'] = function () {
var div = document.body.appendChild(document.createElement('div'));
var lastEvent;
on(div, 'touchstart', function (event) {
// Copying event properties to an object because certain versions of Firefox
// threw insecure operation errors when saving an event to a closure-bound variable.
lastEvent = lang.mixin({}, event);
});
// Since we can't simulate invoking TouchEvents (with current browsers initTouchEvent isn't available)
// we will make TouchEvent be an Event temporarily so the `on` implementation
// thinks that it is one for this test.
var originalTouchEvent = window.TouchEvent;
window.TouchEvent = Event;
on.emit(div, 'touchstart', { changedTouches: [{ pageX: 100 }] });
window.TouchEvent = originalTouchEvent;
assert.property(lastEvent, 'rotation');
assert.property(lastEvent, 'pageX');
});
has('touch') && (suite['DOM-specific']['touch event normalization doesn\'t happen to non-TouchEvent'] = function () {
var div = document.body.appendChild(document.createElement('div'));
var lastEvent;
on(div, 'touchstart', function (event) {
// Copying event properties to an object because certain versions of Firefox
// threw insecure operation errors when saving an event to a closure-bound variable.
lastEvent = lang.mixin({}, event);
});
on.emit(div, 'touchstart', { changedTouches: [{ pageX: 100 }] });
assert.isFalse(lastEvent.pageX === 100);
});
}
registerSuite(suite);
});