I got little problem with combination busterjs+requirejs+backbone, structure of my project:
js-src
--lib //jquery, require, etc
--views
--models
-app.js //require config and start of app
js (compiled same structure as above)
test
-buster.js
-require-config.js
-test-test.js
require-config.js:
require.config({
baseUrl: 'js-src/',
paths: {
jquery: 'lib/jquery',
jplugins: 'lib/jquery.plugins',
underscore: 'lib/underscore',
backbone: 'lib/backbone'
},
shim: {
'backbone': {
deps: ['underscore', 'jplugins'],
exports: 'Backbone'
},
'jplugins': {
deps: ['jquery']
}
}
});
typical file exept off that in lib:
define(function (require) {
var $ = require('jquery'),
Backbone = require('backbone'),
otherElem = require('views/other'),
View = Backbone.View.extend({
el: '#el',
initialize: function () {
},
showLinks: function (value) {
},
render: function ) {
}
});
return View;
});
buster.js:
var config = module.exports;
config['browser-all'] = {
autoRun: false,
environment: 'browser',
rootPath: '../',
libs: [
'js-src/lib/require.js',
'test/require-config.js'
],
sources: [
'js-src/**/*.js'
],
tests: [
'test/*-test.js'
]
// extensions: [
// require('buster-amd')
// ]
};
test-test.js:
buster.spec.expose();
require(['views/View'], function (module) {
describe("An AMD module", function () {
it("should work", function () {
expect(true).toEqual(true);
});
});
});
When i run it using buster test i get:
Uncaught exception: ./js-src/lib/require.js:192 Error: Script error
http://requirejs.org/docs/errors.html#scripterror
TypeError: uncaughtException listener threw error: Cannot read property 'id' of undefined
at Object.uncaughtException (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/progress-reporter.js:49:50)
at notifyListener (/usr/local/lib/node_modules/buster/node_modules/buster-core/lib/buster-event-emitter.js:37:31)
at Object.emit (/usr/local/lib/node_modules/buster/node_modules/buster-core/lib/buster-event-emitter.js:101:17)
at Object.emitCustom (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/remote-runner.js:283:14)
at /usr/local/lib/node_modules/buster/node_modules/buster-test-cli/lib/runners/browser/remote-runner.js:89:16
at /usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/lib/pubsub-client.js:79:47
at Object.trigger (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:383:19)
at Object.distributeMessage (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:666:30)
at Object._deliverMessage (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:1065:20)
at Object.<anonymous> (/usr/local/lib/node_modules/buster/node_modules/buster-test-cli/node_modules/buster-capture-server/node_modules/faye/node/faye-node.js:1004:12)
Firefox 16.0, Linux:
How to write proper test with that structure?
It will help if you run the browser tests and check the outputs in the console. The error messages therein are usually much more expressive. You should also remove the autoRun directive from the buster configuration and re-enable the "buster-amd" extension.
I'm new to Buster.js as of yesterday, but I'll add the following 4 part suggestion.
1.) Uncomment "extensions: [require('buster-amd')]" in your 'buster.js'
2.) Remove 'baseUrl' from your 'require.config'
3.) Explicitly set paths to your libs. For example "jplugins: 'lib/jquery.plugins'" would become "jplugins: 'js-src/lib/jquery.plugins'", this would also be needed for models, collections, views, and other files sitting on the 'js-src/' directory.
require.config({
paths: {
jquery: 'js-src/lib/jquery',
views: 'js-src/lib/views',
somerootfile: 'js-src/somerootfile'
4.) Change your test to be like this ...
describe('some test', function(run) {
require(['models/your_model'], function(YourModel) {
run(function() {
it('should load', function() {
var yourModel = new YourModel();
yourModel.set('cat', 'dog');
expect(YourModel.get('cat')).toEqual('dog');
});
});
});
});
The problem seems to be that the 'baseUrl' in 'require.config' confuse Buster.js and tell it to no long respect the 'rootPath' set in 'buster.js'.
Related
I asked a similar question a while ago when I was trying requireJS here, I'm not sure what I did then is correct or not, but I cannot get it working with systemJS.
I'm trying to create a single module file which contains export for multiple classis for a given module.
This is the sample module (called module1).
import {Module1ComponentA} from "./a.component";
import {Module1ComponentB} from "./b.component";
export * from "./a.component";
export * from "./b.component";
angular.module("module1", [])
.component("module1ComponentA", new Module1ComponentA())
.component("module1ComponentB", new Module1ComponentB())
;
I can then reference either component using
import {Module1ComponentA} from './module1/module1
The problem is I get an exception when loading the app.
Error: TypeError: i.setters[l] is not a function
at exportStar_1 (http://127.0.0.1:8080/output/module1/module1.js:10:9)
at Array.setters.b_component_1 (http://127.0.0.1:8080/output/module1/module1.js:16:17)
at execute (http://127.0.0.1:8080/output/module1/a.component.js:14:13)
Error loading http://127.0.0.1:8080/output/boot.js
If instead I do not include the 'export' statement but references the imports by their source file, then it loads.
This is the SystemJS config
<script>
System.config({
packages: {
output: {
format: 'register',
defaultExtension: 'js'
}
},
map: {
app: 'output/boot.js',
module1: 'output/module1/module1.js'
},
meta: {
'app': { deps: [ 'module1' ] },
'*.js': {
scriptLoad: true,
}
}
});
System.import('output/boot.js')
.then(null, console.error.bind(console));
</script>
This is the generated js file for the module
System.register(["./a.component", "./b.component"], function(exports_1, context_1) {
"use strict";
var __moduleName = context_1 && context_1.id;
var a_component_1, b_component_1;
function exportStar_1(m) {
var exports = {};
for(var n in m) {
if (n !== "default") exports[n] = m[n];
}
exports_1(exports);
}
return {
setters:[
function (a_component_1_1) {
a_component_1 = a_component_1_1;
exportStar_1(a_component_1_1);
},
function (b_component_1_1) {
b_component_1 = b_component_1_1;
exportStar_1(b_component_1_1);
}],
execute: function() {
angular.module("module1", [])
.component("module1ComponentA", new a_component_1.Module1ComponentA())
.component("module1ComponentB", new b_component_1.Module1ComponentB());
}
}
});
//# sourceMappingURL=module1.js.map
Any suggestion on how to do this ?
In my karma.conf.coffee, I have:
files: [
'public/bower_components/angular-mocks/angular-mocks.js'
'public/scripts/**/*.coffee' # not tested since configuration is difficult to be tested and perhaps should not be tested
'test/webapp/unit/**/*.coffee'
'views/templates/*.html'
]
preprocessors:
'public/scripts/**/*.coffee': ['coverage']
'test/webapp/unit/**/*.coffee': ['coffee']
'views/templates/*.html': ['ng-html2js']
frameworks: ['jasmine']
ngHtml2JsPreprocessor:
stripPrefix: 'views/'
In my test, I have:
describe('Directive: timespanSelector', function() {
var scope;
scope = null;
beforeEach(module('myApp'));
beforeEach(module('templates/partialDateRange.html'));
beforeEach(function() {
var html;
html = "<timespan-selector></timespan-selector>";
return inject(function($compile, $rootScope) {
var compiled, elem;
scope = $rootScope.$new();
elem = angular.element(html);
compiled = $compile(elem);
compiled(scope);
return scope.$digest();
});
});
return it('should test', function() {});
});
When I run it, it says:
Error: [$injector:modulerr] Failed to instantiate module templates/partialDateRange.html due to:
Error: [$injector:nomod] Module 'templates/partialDateRange.html' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
http://errors.angularjs.org/1.2.16/$injector/nomod?p0=templates%2FpartialDateRange.html
What am I doing wrong?
This has been an issue for us for a long time and I spent a few days to find out the solution - this it what we came up with. I will simply show you how our file structure is configured.
First off you will need to include karma-ng-html2js-preprocessor.
npm install karma-ng-html2js-preprocessor --save-dev
Next your karma.conf.js - you use coffee but i wont hold it against you ;-)
Remember to include the module name so you can inject it into the directive unit test.
// Karma configuration
// http://karma-runner.github.io/0.10/config/configuration-file.html
module.exports = function (config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
preprocessors: {
'app/views/templates/*.tpl.html': ['ng-html2js'] //<----- Also needed
},
// list of files / patterns to load in the browser
files: [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/bower_components/angular-resource/angular-resource.js',
'app/bower_components/angular-cookies/angular-cookies.js',
'app/bower_components/angular-sanitize/angular-sanitize.js',
'app/bower_components/angular-bootstrap/ui-bootstrap.js',
'app/bower_components/angular-ui-router/release/angular-ui-router.js',
'app/bower_components/angular-local-storage/angular-local-storage.js',
'app/bower_components/jquery/dist/jquery.js',
'app/bower_components/bootstrap/dist/js/bootstrap.js',
'app/scripts/*.js',
'app/scripts/**/*.js',
'test/spec/**/*.js',
//Templates
'app/views/templates/*.tpl.html' //<----- Also needed
],
ngHtml2JsPreprocessor: {
stripPrefix: 'app/',
moduleName: 'Kinetix.Templates' //<-----Module Name for injection
},
// list of files / patterns to exclude
exclude: [],
// Reporters
reporters: ['progress', 'junit'],
//Config for junit
junitReporter: {
outputFile: './test/test-results.xml',
suite: ''
},
// web server port
port: 9001,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};
So then after you got yer karma.conf.js set up lets have a look at a unit test. This is a directive unit test I will include the whole thing as well - maybe it can inspire your for other unit testing ideas as well. I have been at it now for almost 8 months and learned quite a bit...
Directive Unit Test:
'use strict';
describe('Directives: Search', function () {
var//iable declarations
elm,
scope,
$rootScope,
$compile,
$animate,
ACCESS_LEVEL = [
'OPEN',
'PRIVATE',
'RESTRICTED'
]
;
beforeEach(function () {
module('Kinetix.Constants');
module('Kinetix.Templates'); //<------ See here we inject the templates!
module('Kinetix.Directives.Search');
module('Kinetix.Controllers.Search', function ($controllerProvider) {
$controllerProvider.register('SearchController', function () { });
});
});
beforeEach(inject(function (_$rootScope_, _$compile_, _$animate_) {
$rootScope = _$rootScope_;
scope = $rootScope;
$animate = _$animate_;
$compile = _$compile_;
}));
function setupDirective(accessLevel) {
spyOn($animate, 'addClass').and.callThrough();
spyOn($animate, 'removeClass').and.callThrough();
$rootScope.accessLevel = { type: accessLevel };
$rootScope.isAuthenticated = { value: false };
elm = angular.element('<kx-search></kx-search>');
$compile(elm)(scope);
scope.$apply();
}
it('Should create the search template', function () {
setupDirective(ACCESS_LEVEL[0]);
var nav = $(elm).find('.nav');
expect(nav.children('form')).toBeTruthy();
});
describe('Animations', function () {
it('should have the fade slide class on setup with OPEN accesslevel', function () {
setupDirective(ACCESS_LEVEL[0]);
//With Authentication
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect(elm.children('div').hasClass('slide-left')).toBeTruthy();
expect($animate.addClass).toHaveBeenCalled();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('slide-left')).toBeFalsy();
});
it('should toggle the fade-slide animation with PRIVATE acesslevels', function () {
setupDirective(ACCESS_LEVEL[1]);
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeFalsy();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
});
it('should toggle the fade-slide animation with RESTRICTED acesslevels', function () {
setupDirective(ACCESS_LEVEL[2]);
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeFalsy();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
});
});
});
For completion I will also include the directive itself just so you can see the complete picture.
angular.module('Kinetix.Directives.Search', [])
.directive('kxSearch', function ($rootScope, $animate, PATH) {
var linker = function (scope, el) {
var//iable declarations
accessLevel = $rootScope.accessLevel.type || 'Guest',
element = el.children('.nav')
;
//Check if the shop type is a PRIVATE or RESTRICTED type so we can chose which animation to apply
if (accessLevel === 'RESTRICTED' || accessLevel === 'PRIVATE') {
// Hide the element as we need authentication to show it
$animate.addClass(element, 'fade-slide');
$rootScope.$watch('isAuthenticated.value', function (newVal) {
if (!!newVal) {
$animate.removeClass(element, 'fade-slide');
}
if (!newVal) {
$animate.addClass(element, 'fade-slide');
}
});
}
if (accessLevel === 'OPEN') {
$rootScope.$watch('isAuthenticated.value', function (newVal, oldVal) {
if (newVal !== oldVal) {
if(!!newVal) {
$animate.addClass(element, 'slide-left');
}
if(!newVal) {
$animate.removeClass(element, 'slide-left');
}
}
});
}
};
return {
restrict: 'E',
link: linker,
controller: 'SearchController',
templateUrl: PATH.templates + 'search.tpl.html'
};
});
Hope it helps! This was the 800lb gorilla in the room for quite a while, once you actually tame him he is pretty sweet! Good luck!
What am I doing wrong?
Answer: You are attempting to reference an angular module that doesn't exist.
I know this because of the error that you're getting:
Module 'templates/partialDateRange.html' is not available!
Which is caused by the following line of code:
beforeEach(module('templates/partialDateRange.html'));
The Fix
Determine the location of ng-html2js's output. Which is a .js file.
Include that file inside of the files array in karma.conf.coffee
Open the .js file and look for angular.module('<module name>', []) to determine how ng-html2js is naming the module and use that module name where you currently have 'templates/partialDateRange.html' (it should be a similar path name if not the same).
An alternative to karma-ng-html2js-preprocessor is grunt-angular-templates. However, this assumes you are using Grunt (there's also a gulp version floating around).
Add grunt-angular-templates to your package.json file, then simply run grunt ngtemplates from your command line.
This will concatenate all of your templates throughout the application (the ones that are called with templateURL anyways), and put them in a templates.js file.
Then you simply include this file in your karma.conf and you're good to go.
I had a huge amount of trouble setting up karma-ng-html2js-preprocessor and as such, turning to a grunt task was the fastest and easiest way out.
Hope you get it working soon, whichever way you choose to go.
We use RequireJS to add modularity to our Backbone.js site. I found myself with the need to override the Backbone.Collection class to add an advance filtering routine.
My questions is, say I have the following 'override',
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
and our site structure looks like the following:
where, main.js sits at the top level and beneath it is app.js; Where would I add this override, such that I don't have to add a new module to our RequireJS definition for every class? More generally, where are overrides to Backbone usually recommended?
Create a file (say Overrides.js in modules folder)
define(function(require){
var app = require('app');
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
// other overrides can also be added here in this file like
_.extend(Backbone.View.prototype,{}, {
// adding functions or overriding something
})
});
Now, require this file in main.js like
require([
'backbone',
'App',
'modules/Overrides',
'globalize',
.
.
.
],
function ( Backbone, App, ..... ) {
});
There you go!
Say, I want to add some function to the view or override some function such as render, initialize, remove,... universally in the application. You could do something like this:
_.extend(Backbone.View.prototype,{}, {
remove: function() {
alert("View removed");
this.$el.remove();
this.stopListening();
return this;
}
});
One easy option if using requirejs, in your require config add an init statement. eg,
require.config({
shim: {
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone',
init: function (_) {
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
}
}
}
});
Alternatively you can use a map config call to amp all backbone calls to your overridden backbone,
require.config({
map: {
'*': { 'backbone': 'backbone-custom' },
'backbone-custom': { 'backbone': 'backbone' }
}
});
// backbone-custom.js file:
define(['backbone'], function (Backbone) {
Backbone.Collection.prototype.advanceFilter = function() {
/* Filtering code here */
};
return Backbone;
});
Either case will load the override into the backbone object before it is used anywhere.
I'm using an authentication example from here https://github.com/alexanderscott/backbone-login and instead of using underscore templates I would like to use doT.js templates.
I've added the doT.js source to the lib directory and my config.js looks like this
if (typeof DEBUG === 'undefined') DEBUG = true;
require.config({
baseUrl: '/assets',
paths: {
//'jquery' : 'http://code.jquery.com/jquery-1.10.2.js',
'jquery' : 'assets/lib/jquery',
'underscore' : 'assets/lib/underscore', // load lodash instead of underscore (faster + bugfixes)
'backbone' : 'assets/lib/backbone',
'bootstrap' : 'assets/vendor/bootstrap/js/bootstrap',
'doT' : 'assets/lib/doT',
'text' : 'assets/lib/text',
'parsley' : 'assets/lib/parsley'
},
// non-AMD lib
shim: {
//'jquery' : { exports : '$' },
'underscore' : { exports : '_' },
'backbone' : { deps : ['underscore', 'jquery'], exports : 'Backbone' },
'bootstrap' : { deps : ['jquery'], exports : 'Bootstrap' },
'parsley' : { deps: ['jquery'] },
'doT' : { exports : 'doT'}
}
});
require(['main']); // Initialize the application with the main application file.
my app.js looks like this
define([
"jquery",
"underscore",
"backbone",
"doT",
"utils"
],
function($, _, Backbone, doT) {
var app = {
root : "/", // The root path to run the application through.
URL : "/", // Base application URL
API : "/api", // Base API URL (used by models & collections)
// Show alert classes and hide after specified timeout
showAlert: function(title, text, klass) {
$("#header-alert").removeClass("alert-error alert-warning alert-success alert-info");
$("#header-alert").addClass(klass);
$("#header-alert").html('<button class="close" data-dismiss="alert">×</button><strong>' + title + '</strong> ' + text);
$("#header-alert").show('fast');
setTimeout(function() {
$("#header-alert").hide();
}, 7000 );
}
};
$.ajaxSetup({ cache: false }); // force ajax call on all browsers
//alert(doT.template("what up {{=it.name}}"),{'name': 'John'});
// Global event aggregator
app.eventAggregator = _.extend({}, Backbone.Events);
return app;
});
and HeaderView.js looks like this
define([
"app",
"text!templates/header.html",
"utils",
"bootstrap"
], function(app, HeaderTpl){
var HeaderView = Backbone.View.extend({
template: doT.template(HeaderTpl), //_.template(HeaderTpl),
initialize: function () {
_.bindAll(this);
// Listen for session logged_in state changes and re-render
app.session.on("change:logged_in", this.onLoginStatusChange);
},
events: {
"click #logout-link" : "onLogoutClick",
"click #remove-account-link" : "onRemoveAccountClick"
},
onLoginStatusChange: function(evt){
this.render();
if(app.session.get("logged_in")) app.showAlert("Success!", "Logged in as "+app.session.user.get("username"), "alert-success");
else app.showAlert("See ya!", "Logged out successfully", "alert-success");
},
onLogoutClick: function(evt) {
evt.preventDefault();
app.session.logout({}); // No callbacks needed b/c of session event listening
},
onRemoveAccountClick: function(evt){
evt.preventDefault();
app.session.removeAccount({});
},
render: function () {
if(DEBUG) console.log("RENDER::", app.session.user.toJSON(), app.session.toJSON());
this.$el.html(this.template({
logged_in: app.session.get("logged_in"),
user: app.session.user.toJSON()
}));
return this;
},
});
return HeaderView;
});
when i load the page I get the error
Uncaught ReferenceError: doT is not defined
I can call the doT.template() function in the app.js file and I can see that doT.js is loaded in my network tab but when I try and use it in the HeaderView.js I keep getting the error. I am new to require.js so I'm sure I'm misunderstanding how it works.
Looking at the source of doT I see that it calls define by itself. So you do not need a shim configuration for it. Providing a shim for a module that calls define can confuse RequireJS.
Moreover, in the case at hand here, I see that if doT detects that it is an AMD environment (which RequireJS is), then it does not define itself in the global space as doT. So your HeaderView.js file will have to have doT among the required modules. Something like:
define([
"app",
"text!templates/header.html",
"doT",
"utils",
"bootstrap"
], function(app, HeaderTpl, doT){
I have a backbone-extend.js file that I load in the require define in app.js. It has a Backbone.View extender class defining a couple helper methods. Two of the methods work just fine in my views, one always errors with Uncaught TypeError: Object [object global] has no method 'gotoUrl'. Why would just this one method be not defined but the other two are working fine? Do you see any issue in this code...
// Filename: backbone-extend.js
define([
'jquery',
'underscore',
'backbone'
], function($, _, Backbone) {
var helpers = {
eventSyncError: function(model,response,options) {
console.log('Sync error='+response.statusText);
$('#server-message').css({'color':'red', 'font-weight':'bold'}).text(response.statusText);
},
gotoUrl: function(url,delay) {
var to = setTimeout(function() { Backbone.history.navigate(url, true); }, delay);
},
getFormData: function(form) {
var unindexed_array = form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function(n, i) {
indexed_array[n['name']] = n['value'];
});
return indexed_array;
}
}
_.extend(Backbone.View.prototype, helpers);
});
Here is the code in view that calls it...
eventSyncMemberSaved: function(model,response,options) {
console.log("Member saved!");
$('#server-message').css({'color':'green', 'font-weight':'bold'}).text("Member saved!");
this.gotoUrl('members',2000);
//setTimeout(function() { Backbone.history.navigate('members', true); }, 2000);
},
saveMember: function() {
var data = this.getFormData($('#member-form'));
this.member.save(data, { success: this.eventSyncMemberSaved });
},
Thanks in advance for your help. I'm stuck.
The context of this is different in the success callback.
It no longer points to the view as it points to the xhr object
So it throws an error as that method is not available on the xhr object
To resolve it you need to bind the context of this to the success handler so that it points to the right object.
So in the initialize of the view add this code
initialize: function() {
_.bindAll(this, 'eventSyncMemberSaved');
}