Skip to content

Commit

Permalink
[FEATURE canary] Fire native events instead of jquery events
Browse files Browse the repository at this point in the history
Goal: To fire events in relatively accurate and cross-browser way.

Simulate real events in pure javascript accurately is nearly impossible, but
this is a best effort. This fires 3 different kind of events:

- MouseEvents: click, dbclick, mousedown, mouseup, mouseenter, mouseleave, ...
- KeyEvents: keydown, keypress and keyup
- Events: Anything else.

This should probably cover 99% of use cases.
  • Loading branch information
cibernox committed Feb 3, 2016
1 parent c703cf6 commit 3fbdcb7
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 39 deletions.
5 changes: 5 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ for a detailed explanation.

When the proper API's are implemented by the resolver in use this feature allows `{{x-foo}}` in a
given routes template (say the `post` route) to lookup a component nested under `post`.
* `ember-test-helpers-fire-native-events`
Makes ember test helpers (`fillIn`, `click`, `triggerEvent` ...) fire native javascript events instead
of `jQuery.Event`s, maching more closely app's real usage.
1 change: 1 addition & 0 deletions features.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"features-stripped-test": null,
"ember-htmlbars-component-generation": false,
"ember-application-visit": true,
"ember-test-helpers-fire-native-events": true,
"ember-routing-route-configured-query-params": null,
"ember-libraries-isregistered": null,
"ember-debug-handlers": true,
Expand Down
180 changes: 150 additions & 30 deletions packages/ember-testing/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import run from 'ember-metal/run_loop';
import jQuery from 'ember-views/system/jquery';
import Test from 'ember-testing/test';
import RSVP from 'ember-runtime/ext/rsvp';
import isEnabled from 'ember-metal/features';

/**
@module ember
Expand All @@ -13,6 +14,141 @@ import RSVP from 'ember-runtime/ext/rsvp';
var helper = Test.registerHelper;
var asyncHelper = Test.registerAsyncHelper;

var keyboardEventTypes, mouseEventTypes, buildKeyboardEvent, buildMouseEvent, buildBasicEvent, fireEvent, focus;

if (isEnabled('ember-test-helpers-fire-native-events')) {
let defaultEventOptions = { canBubble: true, cancelable: true };
keyboardEventTypes = ['keydown', 'keypress', 'keyup'];
mouseEventTypes = ['click', 'mousedown', 'mouseup', 'dblclick', 'mousenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover'];


buildKeyboardEvent = function buildKeyboardEvent(type, options = {}) {
let event;
try {
event = document.createEvent('KeyEvents');
let eventOpts = jQuery.extend({}, defaultEventOptions, options);
event.initKeyEvent(
type,
eventOpts.canBubble,
eventOpts.cancelable,
window,
eventOpts.ctrlKey,
eventOpts.altKey,
eventOpts.shiftKey,
eventOpts.metaKey,
eventOpts.keyCode,
eventOpts.charCode
);
} catch (e) {
event = buildBasicEvent(type, options);
}
return event;
};

buildMouseEvent = function buildMouseEvent(type, options = {}) {
let event;
try {
event = document.createEvent('MouseEvents');
let eventOpts = jQuery.extend({}, defaultEventOptions, options);
event.initMouseEvent(
type,
eventOpts.canBubble,
eventOpts.cancelable,
window,
eventOpts.detail,
eventOpts.screenX,
eventOpts.screenY,
eventOpts.clientX,
eventOpts.clientY,
eventOpts.ctrlKey,
eventOpts.altKey,
eventOpts.shiftKey,
eventOpts.metaKey,
eventOpts.button,
eventOpts.relatedTarget);
} catch (e) {
event = buildBasicEvent(type, options);
}
return event;
};

buildBasicEvent = function buildBasicEvent(type, options = {}) {
let event = document.createEvent('Events');
event.initEvent(type, true, true);
jQuery.extend(event, options);
return event;
};

fireEvent = function fireEvent(element, type, options = {}) {
if (!element) {
return;
}
let event;
if (keyboardEventTypes.indexOf(type) > -1) {
event = buildKeyboardEvent(type, options);
} else if (mouseEventTypes.indexOf(type) > -1) {
let rect = element.getBoundingClientRect();
let x = rect.left + 1;
let y = rect.top + 1;
let simulatedCoordinates = {
screenX: x + 5,
screenY: y + 95,
clientX: x,
clientY: y
};
event = buildMouseEvent(type, jQuery.extend(simulatedCoordinates, options));
} else {
event = buildBasicEvent(type, options);
}
element.dispatchEvent(event);
};

focus = function focus(el) {
if (!el) { return; }
let $el = jQuery(el);
if ($el.is(':input, [contenteditable=true]')) {
let type = $el.prop('type');
if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') {
run(null, function() {
// Firefox does not trigger the `focusin` event if the window
// does not have focus. If the document doesn't have focus just
// use trigger('focusin') instead.

if (!document.hasFocus || document.hasFocus()) {
el.focus();
} else {
$el.trigger('focusin');
}
});
}
}
};
} else {
focus = function focus(el) {
if (el && el.is(':input, [contenteditable=true]')) {
var type = el.prop('type');
if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') {
run(el, function() {
// Firefox does not trigger the `focusin` event if the window
// does not have focus. If the document doesn't have focus just
// use trigger('focusin') instead.
if (!document.hasFocus || document.hasFocus()) {
this.focus();
} else {
this.trigger('focusin');
}
});
}
}
};

fireEvent = function fireEvent(element, type, options) {
var event = jQuery.Event(type, options);
jQuery(element).trigger(event);
};
}


function currentRouteName(app) {
var routingService = app.__container__.lookup('service:-routing');

Expand All @@ -36,24 +172,6 @@ function pauseTest() {
return new RSVP.Promise(function() { }, 'TestAdapter paused promise');
}

function focus(el) {
if (el && el.is(':input, [contenteditable=true]')) {
var type = el.prop('type');
if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') {
run(el, function() {
// Firefox does not trigger the `focusin` event if the window
// does not have focus. If the document doesn't have focus just
// use trigger('focusin') instead.
if (!document.hasFocus || document.hasFocus()) {
this.focus();
} else {
this.trigger('focusin');
}
});
}
}
}

function visit(app, url) {
var router = app.__container__.lookup('router:main');
var shouldHandleURL = false;
Expand All @@ -78,13 +196,15 @@ function visit(app, url) {
}

function click(app, selector, context) {
var $el = app.testHelpers.findWithAssert(selector, context);
run($el, 'mousedown');
let $el = app.testHelpers.findWithAssert(selector, context);
let el = $el[0];

focus($el);
run(null, fireEvent, el, 'mousedown');

run($el, 'mouseup');
run($el, 'click');
focus(el);

run(null, fireEvent, el, 'mouseup');
run(null, fireEvent, el, 'click');

return app.testHelpers.wait();
}
Expand Down Expand Up @@ -119,10 +239,9 @@ function triggerEvent(app, selector, contextOrType, typeOrOptions, possibleOptio
}

var $el = app.testHelpers.findWithAssert(selector, context);
var el = $el[0];

var event = jQuery.Event(type, options);

run($el, 'trigger', event);
run(null, fireEvent, el, type, options);

return app.testHelpers.wait();
}
Expand All @@ -143,18 +262,19 @@ function keyEvent(app, selector, contextOrType, typeOrKeyCode, keyCode) {
}

function fillIn(app, selector, contextOrText, text) {
var $el, context;
var $el, el, context;
if (typeof text === 'undefined') {
text = contextOrText;
} else {
context = contextOrText;
}
$el = app.testHelpers.findWithAssert(selector, context);
focus($el);
el = $el[0];
focus(el);
run(function() {
$el.val(text);
$el.trigger('input');
$el.change();
fireEvent(el, 'input');
fireEvent(el, 'change');
});
return app.testHelpers.wait();
}
Expand Down
53 changes: 44 additions & 9 deletions packages/ember-testing/tests/helpers_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,6 @@ QUnit.test('`click` triggers appropriate events in order', function() {
['mousedown', 'focusin', 'mouseup', 'click'],
'fires focus events on contenteditable');
}).then(function() {
// In IE (< 8), the change event only fires when the value changes before element focused.
jQuery('.index-view input[type=checkbox]').focus();
events = [];
return click('.index-view input[type=checkbox]');
}).then(function() {
Expand All @@ -407,6 +405,44 @@ QUnit.test('`click` triggers appropriate events in order', function() {
});
});

QUnit.test('`click` triggers native events with simulated X/Y coordinates', function() {
expect(15);

var click, wait, events;

App.IndexView = EmberView.extend({
classNames: 'index-view',

didInsertElement() {
let pushEvent = e => events.push(e);
this.element.addEventListener('mousedown', pushEvent);
this.element.addEventListener('mouseup', pushEvent);
this.element.addEventListener('click', pushEvent);
}
});


Ember.TEMPLATES.index = compile('some text');

run(App, App.advanceReadiness);

click = App.testHelpers.click;
wait = App.testHelpers.wait;

return wait().then(function() {
events = [];
return click('.index-view');
}).then(function() {
events.forEach(e => {
ok(e instanceof window.Event, 'The event is an instance of MouseEvent');
ok(typeof e.screenX === 'number' && e.screenX > 0, 'screenX is correct');
ok(typeof e.screenY === 'number' && e.screenY > 0, 'screenY is correct');
ok(typeof e.clientX === 'number' && e.clientX > 0, 'clientX is correct');
ok(typeof e.clientY === 'number' && e.clientY > 0, 'clientY is correct');
});
});
});

QUnit.test('`wait` waits for outstanding timers', function() {
expect(1);

Expand All @@ -423,7 +459,6 @@ QUnit.test('`wait` waits for outstanding timers', function() {
});
});


QUnit.test('`wait` respects registerWaiters with optional context', function() {
expect(3);

Expand Down Expand Up @@ -470,7 +505,7 @@ QUnit.test('`triggerEvent accepts an optional options hash without context', fun
template: compile('{{input type="text" id="scope" class="input"}}'),

didInsertElement() {
this.$('.input').on('blur change', function(e) {
this.$('.input').on('keydown change', function(e) {
event = e;
});
}
Expand All @@ -482,10 +517,10 @@ QUnit.test('`triggerEvent accepts an optional options hash without context', fun
wait = App.testHelpers.wait;

return wait().then(function() {
return triggerEvent('.input', 'blur', { keyCode: 13 });
return triggerEvent('.input', 'keydown', { keyCode: 13 });
}).then(function() {
equal(event.keyCode, 13, 'options were passed');
equal(event.type, 'blur', 'correct event was triggered');
equal(event.type, 'keydown', 'correct event was triggered');
equal(event.target.getAttribute('id'), 'scope', 'triggered on the correct element');
});
});
Expand Down Expand Up @@ -650,7 +685,7 @@ QUnit.test('`triggerEvent accepts an optional options hash and context', functio
template: compile('{{input type="text" id="outside-scope" class="input"}}<div id="limited">{{input type="text" id="inside-scope" class="input"}}</div>'),

didInsertElement() {
this.$('.input').on('blur change', function(e) {
this.$('.input').on('keydown change', function(e) {
event = e;
});
}
Expand All @@ -663,11 +698,11 @@ QUnit.test('`triggerEvent accepts an optional options hash and context', functio

return wait()
.then(function() {
return triggerEvent('.input', '#limited', 'blur', { keyCode: 13 });
return triggerEvent('.input', '#limited', 'keydown', { keyCode: 13 });
})
.then(function() {
equal(event.keyCode, 13, 'options were passed');
equal(event.type, 'blur', 'correct event was triggered');
equal(event.type, 'keydown', 'correct event was triggered');
equal(event.target.getAttribute('id'), 'inside-scope', 'triggered on the correct element');
});
});
Expand Down

0 comments on commit 3fbdcb7

Please sign in to comment.