Reputation: 8950
I've got two RequireJS modules, one for fetching data from an external service, one in charge of passing a callback to the first module.
Here is the first very basic module:
define(["jquery"], function($) {
return {
/**
* Retrieves all the companies that do not employs the provided employee
* @param employeeId ID of the employee
* @param successCallback callback executed on successful request completion
* @return matching companies
*/
fetchCompanies: function(employeeId, successCallback) {
var url = '/employees/' + employeeId + '/nonEmployers';
return $.getJSON(url, successCallback);
}
};
});
And the most interesting one, that will generate a new drop-down and inject it into the specified DOM element (this is the one under test):
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
var updateCompanies = function(selectedEmployeeId, companyDropDownSelector) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
$(companyDropDownSelector).html(newContents);
});
};
return {
/**
* Updates the dropdown identified by companyDropDownSelector
* with the companies that are non employing the selected employee
* @param employeeDropDownSelector selector of the employee dropdown
* @param companyDropDownSelector selector of the company dropdown
*/
observeEmployees: function(employeeDropDownSelector, companyDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
updateCompanies(selectedEmployeeId, companyDropDownSelector);
}
});
}
};
});
I'm trying to test this last module, using Jasmine-fixtures and using waitsFor, to asynchronously check that the set-up test DOM structure has been modified. However, the timeout is always reached.
If you can spot what's wrong in the following test, I'd be most grateful (gist:https://gist.github.com/fbiville/6223bb346476ca88f55d):
define(["jquery", "modules/non-employers", "modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
});
it("should update the company dropdown", function() {
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
activities.observeEmployees('#employees', '#companies');
$('#employees').trigger('change');
waitsFor(function() {
var companiesContents = $('#companies').html(),
result = expect(companiesContents).toContain('<option value="42">ACME</option>');
return result && expect(companiesContents).toContain('<option value="100">OUI</option>');
}, 'DOM has never been updated', 10000);
});
});
});
Thanks in advance!
Rolf
P.S.: replacing $(employeeDropDownSelector).change
by $(employeeDropDownSelector).on('change',
and/or wrapping the activities.observeEmployees
call (and $('#employees').trigger('change');
) with a domReady yields the same result
P.P.S.: this error is the cause -> SEVERE: runtimeError: message=[An invalid or illegal selector was specified (selector: '[id='employees'] :selected' error: Invalid selector: *[id="employees"] *:selected).] sourceName=[http://localhost:59811/src/vendor/require-jquery.js] line=[6002] lineSource=[null] lineOffset=[0]
.
P.P.P.S.: it seems HtmlUnit doesn't support CSS3 selectors (WTF?), and even forcing the latest published version as jasmine-maven-plugin dependency won't change anything...
Is there any way to change jasmine plugin runner ?
Upvotes: 1
Views: 917
Reputation: 18408
Creating modules this way is really difficult. I'd recommend not using fixtures and not rendering anywhere actually. Instead using detached DOM elements to do all the work is much easier.
Imagine if your code looked closer to this:
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
return {
init: function() {
this.$companies = $('<select class="js-companies"></select>');
},
render: function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
this.$companies.empty().append(newContents);
return this;
});
observeEmployees: function(employeeDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
this.render(data);
}
}
});
}
};
});
The above is not complete. It is just to give you an idea of another way to approach your problem. Now instead of a fixture all you need to do is inspect this.$companies
and you will be done. I think the main problem though is that your functions are not simple enough. The concern of each function should be extremely specific. Your updateCompanies function is doing things like creating a template, fetching data then passing it to an anonymous function, which can't be spied on, that anonymous function iterates on an object, then you change some already existing DOM element. That sounds exhausting. All that function should do is look at some precompiled template send it an object. The template should loop on the object using {{each}} then return. Your function then empties and append the newContents and returns it self so the next function down can choose what it should do with this.$companies. Or if this.$companies has already been append to the page nothing needs to be done at all.
Upvotes: 1
Reputation: 8950
OK guys.
Solution found:
$(mySelector).get(0) == document.activeElement
run(function() { /* expect */ })
if they are positioned after and depend on your waitsFor
condition.Finally, all should be well.
See how is the test now:
define(["jquery",
"modules/nonEmployers",
"modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
});
it("should update the company dropdown", function() {
$(document).ready(function() {
activities.observeEmployees('#employees', '#companies');
$('#employees option[selected=selected]').removeAttr("selected");
$('#employees option[value=1]').attr("selected", "selected");
$('#employees').trigger('change');
waitsFor(function() {
var dropDown = $('#companies').html();
return dropDown.indexOf('ACME') > 0 && dropDown.indexOf('OUI') > 0;
}, 'DOM has never been updated', 500);
runs(function() {
var dropDown = $('#companies').html();
expect(dropDown).toContain('<option value="42">ACME</option>');
expect(dropDown).toContain('<option value="100">OUI</option>');
});
});
});
});
});
Upvotes: 1