Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make acceptance helpers fire native events instead of jQuery ones. #12575

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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