Comment AngularJS sait-il quelles services doivent-être passés au contrôleur ?
angular.module('todo', [])
.controller('TodoController', function($scope, $routeParams) {
$scope.id = $routeParams.id;
// ...
});
Et en cas de minification ?
angular.module('todo', [])
.controller('TodoController',
['$scope', '$routeParams', function($scope, $routeParams) {
$scope.id = $routeParams.id;
// ...
}]);
Le service $injector
s'occupe de repérer les dépendances nécessaire.
En cas de minifaction, le nom des variable est modifié pour être raccourci. Il faut donc préciser les services nécessaires.
Écriture explicite préféré pour être sur de ses dépendances.
Un module :
ngApp
).Une application :
// Créer le module 'nom'
// Le deuxième paramètre est la liste des dépendances du module.
angular.module('nom', []);
// Récupèrer le module.
angular.module('nom');
Exemple de dépendances : ngRoute
.
angular-seed
est un bon modèle :
angular-seed
a un dossier components
comprenants les filtres et directives.
README
LICENSE
bower.json
package.json
app/
├── app.js
├── css/
│ └── app.css
├── img/
├── index.html
└── todo/
├── partials/
│ ├── todo_list.html
│ └── todo_single.html
├── todo.js
└── todo_test.js
angular-seed
.Un service :
angular.module('todo', [])
.controller('TodoController',
['$routeParams', function($routeParams) {
// ...
}]);
Un singleton est un objet qui n'est créé qu'une fois. Il n'est donc présent qu'une fois en mémoire et c'est le même qui est toujours utilisé.
$routeParams
est un service. $scope
est un cas particulier.
Les services du core commencent par $
.
$filter
: Appliquer des filtres.$http
: Faire des requêtes HTTP.$interpolate
: Évaluer des expressions.$location
: Récupérer ou agir sur l'URL.$q
: Faire des promesses.$rootScope
$document
, $window
, $timeout
, $interval
$document
, $window
, $timeout
, $interval
sont des wrappers pour leur équivalent JS.
Ou comment créer un service.
Une factory
retourne le service demandé.
angular.module('myApp', [])
.factory('hello', function() {
return {
hello: function(name) {
return "Hello " + name;
},
};
});
service
peut être utilisé en tant que constructeur du service.
angular.module('myApp', [])
.service('hello', function() {
this.hello = function(name) {
return "Hello " + name;
};
});
Service déjà créer et accessible par this
.
angular.module('myApp', [])
.constant('version', '0.1');
angular.module('myApp', [])
.value('version', '0.1');
constant
peut être utilisé lors d'un config
alors que value
ne peut pas.
angular.module('myApp', [])
.constant('version', '0.1');
.config(['version', function(version) {
// Ok
}])
angular.module('myApp', [])
.value('version', '0.1');
.config(['version', function(version) {
// Error
}])
Un provider n'est accessible que durant la phase de configuration (et donc dans les .config()
).
$provide
permet de définir ou redéfinir un service dynamiquement.
angular.module('myApp', [])
.provider('ServiceProvider', function () {
this.$get = function () {
return {
myService: True,
};
};
});
.run('TestController', function($provide) {
$provide.factory('ServiceProvider', function() {
return {
myService: False,
};
});
})
provider
est utilisé par les autres systemes de création de services.
id
.npm install karma karma-chrome-launcher
./node_modules/.bin/karma init karma.conf.js
./node_modules/.bin/karma start karma.conf.js
sudo npm install -g karma-cli
karma start
# Ou avec angular-seed
npm test
npm install
permet d'installer les paquets Node.js.
Les paquets sont installé dans node_modules
et les binaires se retrouvent dans node_modules/.bin
.
karma-cli
permet d'avoir un executable karma
qui va chercher le binaire karma courant.
npm test
permet de lancer le script test
qui se trouve dans package.json
.
npm install karma-jasmine
"behavior-driven" signifie que l'on décrit les composants à tester, ce qu'il devrait faire et ce que l'on attends d'eux.
describe('Unit test: TodoController', function(){
// Specs go in here
});
describe('Unit test: TodoController', function(){
describe('list', function(){
// Specs go in here
});
});
describe('Unit test: TodoController', function(){
it('should be true', function() {
expect(true).toBe(true);
});
});
describe('Unit test: TodoController', function(){
var a;
it('should be true', function() {
a = true;
expect(a).toBe(true);
});
});
describe("A spec (with setup and tear-down)", function() {
var foo;
beforeEach(function() {
foo = 0;
foo += 1;
});
afterEach(function() {
foo = 0;
});
it("is just a function, so it can contain any code", function() {
expect(foo).toEqual(1);
});
it("can have more than one expectation", function() {
expect(foo).toEqual(1);
expect(true).toBeTruthy();
});
});
Effectuer certaines taches avant et/ou après chaque test.
L'API a changé entre Jasmine 1.x et 2.x !
Permet de simuler des fonctions/objets le temps d'un describe
/it
.
// Jasmine 2.0
service = {
test: function(foo, bar) {
return 42;
}
};
SpyOn(service, 'test');
service.test(1, 2);
expect(service.test).toHaveBeenCalled();
expect(service.test).toHaveBeenCalledWith(1, 2);
SpyOn(service, 'test').and.returnValue(21);
var test = service.test(1, 2);
expect(test).toEqual(21);
// Jasmine 2.0
jasmine.createSpy('foobar');
foobar('foo', 'bar');
expect(foobar).toHaveBeenCalledWith('foo', 'bar');
// Créer un objet avec plusieurs spies
jasmine.createSpy('service', ['create', 'update', 'delete']);
service.create();
expect(service.create).toHaveBeenCalled();
Un mock permet de tester et de simuler le fonctionnement d'un composant métier.
Pensez à inclure angular-mocks.js
dans la configuration de karma
.
angular.module('myApp', [])
.value('version', 'v1.0.1');
describe('MyApp', function() {
// Charger le module 'myApp'
beforeEach(module('myApp'));
// Puis injecter le service 'version'
it('should provide a version', inject(function(version) {
expect(version).toEqual('v1.0.1');
}));
});
beforeEach(module('myApp'));
permet de définir les modules contenants les composants à injecter.
Il est possible d'injecter des composants en utilisant angular.mock.inject
.
angular.module('myApp', []).value('version', 'v1.0.1');
describe('MyApp', function() {
var version;
beforeEach(module('myApp'));
beforeEach(inject(function(_version_){
version = _version_;
}));
it('should provide a version', function() {
expect(version).toEqual('v1.0.1');
});
});
describe('MyApp - $provide', function() {
it('should provide a version', function(version) {
module(function($provide) {
$provide.value('version', 'VERSION');
})
inject(function(version) {
expect(version).toEqual('VERSION');
});
});
});
inject
est également présent sur window
et peut donc être accédé directement.
On peut utiliser inject
dans un beforeEach
pour avoir le composant dans tout les tests.
Pour ne pas surcharger la variable local, le service peut être injécté avec des "_" autour.
$provide
permet de remplacer un service.
describe('Unit test: controller', function(){
var MyController, scope;
beforeEach(module('myApp'));
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
MyController = $controller('MyController', {$scope: scope});
}));
});
Le service $controller
permet de créer un nouveau contrôleur.
Les services injecté peuvent être passé en paramètres afin de les contrôler.
describe('Unit test: Service', function(){
var service;
beforeEach(module('myApp'));
beforeEach(inject(function(_service_){
service = _service_;
});
});
Le service sera injecté dans tout les tests.
describe('Unit test: Service', function(){
var filter;
beforeEach(module('myApp'));
beforeEach(inject(function($filter){
filter = $filter;
});
it('should works', function(){
expect(filter('number')(123, 2).toEqual('123.00'));
});
});
$filter
est utilisé pour récupérer tout les filtres.
describe('Unit test: Service', function(){
var element, scope;
beforeEach(module('myApp'));
beforeEach(inject(function($compile, $rootScope){
scope = $rootScope.$new();
element = angular.element('<my-directive></my-directive>');
$compile(element)(scope);
scope.$apply();
});
it('should works', function(){
scope.$apply(function(){
scope.value = 'new value';
});
expect(element.html()).toContain('new value');
});
});
Créer un nouvel element HTML avec angular.
$compile
nous permet de transformer le texte HTML avec angular.
$apply
permet d'executer du code dans angular en dehors du framework.
Il execute aussi les divers méthodes de watch
($digest
) et permet donc de mettre à jour les élements compilés.
angular-seed
.todo
.Le serveur doit tourner pour permettre de tester comme un véritable utilisateur.
npm install protractor
./node_modules/.bin/webdriver-manager update
Met à jour les drivers utilisés pour contrôler les navigateurs.
protractor.conf.js
exports.config = {
specs: [
'*.js'
],
capabilities: {
'browserName': 'chrome',
// A partir de chrome 35
'chromeOptions': {
args: ['--test-type']
},
},
baseUrl: 'http://localhost:8000/app/',
framework: 'jasmine',
};
specs
: fichier de tests protractor (Il n'y a plus d'accès direct aux fichiers de l'application).
capabilities
: les navigateurs sur lesquelles tester.
baseUrl
: L'URL de l'application.
# Le serveur doit être lancé pour pouvoir lancer les tests fonctionnels
./node_modules/.bin/protractor protractor.conf.js
!console
# Ou avec angular-seed
npm run-script protractor
describe('angularjs homepage', function() {
var firstNumber = element(by.model('first'));
var secondNumber = element(by.model('second'));
var goButton = element(by.id('gobutton'));
var latestResult = element(by.binding('latest'));
var history = element.all(by.repeater('result in memory'));
function add(a, b) {
firstNumber.sendKeys(a);
secondNumber.sendKeys(b);
goButton.click();
}
beforeEach(function() {
browser.get('http://juliemr.github.io/protractor-demo/');
});
it('should have a history', function() {
add(1, 2);
add(3, 4);
expect(history.count()).toEqual(2);
});
});
Les locators
sont des méthodes permettant de récuperer les éléments de la page HTML.
Bonne pratique de préparer les elements avant et de faire des fonctions pour les parties logiques réutilisées.
Les éléments ne seront trouvé qu'au moment de l'action.
describe('Unit Test: HTTP', function() {
var $httpBackend, myService;
beforeEach(inject(function(_$httpBackend_, _myService_){
$httpBackend = _$httpBackend_;
// Imaginons que myService est un service faisant des requêtes pour nous.
myService = _myService_;
}));
afterEach(function(){
// Il faut s'assurer qu'il ne reste pas de requêtes
// ou d'attentes à la fin de chaque test.
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should make a request', function(){
$httpBackend.expect('GET', '/v1/api/current_user')
.respond(200, {userId: 123});
myService.getCurrentUser();
$httpBackend.flush();
});
});
Dans le cas des tests unitaires, il faut pouvoir simuler un serveur HTTP pour ne tester que la fonctionnalité recherchée et pas la connexion ou le serveur distant.
$httpBackend.expect()
vérifie que la requête soit bien partis et permet de gérer la réponse.
$httpBackend.flush()
permet de s'assurer que les réponses soit bien envoyées.
$httpBackend.verifyNoOutstandingExpectation()
et $httpBackend.verifyNoOutstandingRequest()
vont s'occuper de ces vérifications.
angular-seed
.todo
.Une directive permet d'étendre le language HTML.
Les formes les plus courantes de directives sont les suivantes :
<div my-directive="value"></div>
<div my-directive></div>
<my-directive></my-directive>
angular.module('myApp', [])
.directive('myDirective', function() {
return {
// Options
};
});
restrict
: Indique de quelle manière une directive peut être utilisé. A
pour attribut et E
pour element.template
/templateUrl
: Template ou url vers le template ("partial") à utiliser.replace
: Si true
, le template remplace le block plutot que d'etre ajouté à la fin.scope
: Si true
, un nouveau scope sera créer pour la directive. Peut également être un objet décrivant les valeurs du scope.controller
: Le contrôleur à utiliser pour gérer la directive.Fichier JS :
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'AE',
template: '<a href="google.com">Google</a>',
replace: true,
};
});
Fichier HTML :
<div my-directive></div>
<my-directive></my-directive>
Résultat :
<a href="google.com">Google</a>
<a href="google.com">Google</a>
Noter que la directive est créer en camelCase mais est utilisé en lower-case.
Fichier JS :
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'partials/my-directive.html',
scope: {
value: '=',
}
};
});
Fichier HTML :
<my-directive value="'foobar'"></my-directive>
Fichier partials/my-directive.html
:
{{ value }}
Résultat :
<my-directive value="'foobar'">foobar</my-directive>
Les directives permettent d'étendre le langage HTML.
Les filtres permettent de modifier la manière dont les données sont affichées.
Les services mettent à disposition du code métier.
Plus un composant est générique, plus il est réutilisable. Au contraire, un composant métier qui n'est utilisé qu'une fois n'a pas besoin d'être générique. Il faut trouver le juste milieu.
Voir le dossier components
de angular-seed
.
Composants générique = plus de temps de dév la première fois et moins les suivantes.
Table of contents | t |
---|---|
Exposé | ESC |
Autoscale | e |
Full screen slides | f |
Presenter view | p |
Source files | s |
Slide numbers | n |
Blank screen | b |
Notes | 2 |
Help | h |