Archives par mot-clé : angularjs

Angular/Ionic: simuler un serveur avec des fichiers JSON

Je travaille actuellement sur un projet mobile Ionic et j’ai du mettre en place un mode démonstration avec des données fake. Au lieu de faire un appel au serveur, j’utilise des fichiers JSON qui contiennent les données à retourner.

Cela peut également être pratique si vous devez travailler sans être dépendant de la partie serveur.
Afin d’éviter de toucher le code métier (mes services angular) et faire des tests dans mes controllers/services pour savoir si je suis en mode démo ou pas, j’utilise un interceptor angular (la documentation angular sur $http et les interceptors) qui va faire la requête http ou renvoyer les bonnes de démonstration.

L’astuce ici consiste à contourner le cache de $http. Il faut modifier dans la fonction request de l’interceptor l’objet config pour définir une fonction get dans le cache. (Seulement les verbes HTTP GET et JSONP peuvent utiliser le cache $http. Je force un appel en GET mais garde en mémoire la méthode d’origine). Ainsi, l’appel serveur ne sera pas fait et la fonction définie en tant que cache sera appelée. Vous pouvez ainsi retourner vos données, dans notre cas à partir de fichiers json.

Voici le code de mon interceptor:

(function (module) {
  'use strict';
  module.factory('demoInjector', ['baseUrl', '$injector', function(baseUrl, $injector) {

    //check if request is done on backend and not templates for example 
    const isBackendRequest = function (config) {
      return config.url.indexOf(baseUrl) > -1;
    };

    var sessionInjector = {
      request: function(config) {
        if (!isBackendRequest(config)) {
          return config;
        }
        const demoService = $injector.get('DemoService');
        if (demoService.isDemoMode) {
          //store http method and change request method to GET because only GET and JSONP are cached
          config.originalMethod = config.method;
          config.method = 'GET';
          config.cache = {
            get: function() {
              return demoService.mock(config);
            }
          };
        }
        return config;
      }
    };
    return sessionInjector;
  }]);

  module.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.unshift('demoInjector');
  }]);
})(angular.module('diagral'));

Dans le demoService, service qui contient la logique du mode démonstration, on se base sur l’url (via une RegExp) et la méthode HTTP (sauvegardée dans la propriété originalMethod via l’interceptor) de l’objet config pour déterminer le contenu approprié.

Voyons maintenant comment charger un fichier JSON inclus dans votre projet Ionic (en version 1). Pour ma part, les fichiers sont stockés dans /www/demo.
Sous Android, le dossier www est placé dans le dossier android_asset. Il faut donc adapter l’url du fichier cible en prenant compte de la platform cible via ionic.Platform. Il suffit ensuite de faire une requête via $http avec le chemin du fichier.

Les exemples ci-dessous sont en ES2015 avec des classes (d’où les this …)

this.url = '';
if (ionic.Platform.isWebView() && ionic.Platform.isAndroid()) {
    this.url = '/android_asset/www/';
}

this.$http.get(this.url + 'demo/myFile.json');

Afin de simuler un temps d’attente réaliste, vous pouvez utiliser $timeout.

const delay = 1000; //délai d'1 seconde
const responseDeferred = this.$q.defer();
this.$timeout(() => {
  this.$http.get(this.url + 'demo/myFile.json')
    .then(resp => responseDeferred.resolve(resp));
}, delay);
return responseDeferred.promise;

Il est également possible de renvoyer un objet JavaScript directement (si vous avez besoin de gérer un état par exemple). Il faut, dans ce cas, créer une réponse comme ceci:

const response = {
  config: config,
  status: 200,
  headers: () => {},
  data: () => angular.toJson({
          name: 'Julien',
          age: 35,
          lastUpdate: Date.now()
        })  
};

Voici le code final de la fonction mock qui utilise un tableau de configuration contenant les mocks avec la réponse attendue et les différentes options (délai, json/fonction, …).

mock(config) {
  const response = {
	config: config,
	status: 200
  };

  //Configuration du tableau des mocks
  const staticResponses = [
  {
    pattern: /login$/,
    jsonFile: 'login.json',
    method: 'POST'
  },
  {
    pattern: /article$/,
    data: () => this.getFakeArticles(),
    delay: 500
  },
  //...
  ];
  const staticResponse = this.findMock(staticResponses, config);
  if (staticResponse) {
    const r = this.$q.defer();
    const { delay = 200 } = staticResponse; //200ms by default if not defined
    this.$timeout(() => {
      if (staticResponse.jsonFile) {
        this.$http.get(this.url + 'demo/' + staticResponse.jsonFile)
          .then(resp => r.resolve(resp));
        return;
      }
      if (this._.isFunction(staticResponse.data)) {
        response.data = staticResponse.data(config.data, config.url);
        r.resolve(response);
        return;
      }
    }, delay);
    return r.promise;
  }

  response.status = 500;
  response.data = { message: 'Indisponible en mode démo'};
  return this.$q.reject(response);
}

findMock(staticResponses, { url: requestUrl, originalMethod: method}) {
  return this._.find(staticResponses, r => r.pattern.test(requestUrl) && method === (r.method || 'GET'));
}

Components en angular 1.5

Un des grands changement d’Angular 2 est l’abandon des controller pour l’utilisation de components. Afin de facilité la migration, la version 1.5 d’Angular a introduit une méthode component() permettant de créer des composants. Cette version introduit également les concepts de one-way data binding et de lifecycle hooks.

Création d’un component

La création d’un composant est très simple. il suffit de renvoyer un objet comme dans l’exemple ci dessous.

var ProductComponent = {
  bindings: {
    name: '=',
    price: '='
  },
  template: `Name: {{$ctrl.name}}
             Price: {{$ctrl.price}}`
};

angular
  .module('app', [])
  .component('productComponent', ProductComponent);
<product-component name="Nexus 5" qty="299"></product-component>

On peut voir dans l’exemple ci dessus:

  • la déclaration des bindings via la propriété … bindings. Nous les déclarons de la même façon que pour les directives. Les bindings sont automatiquement bindés sur le controller (bindToController dans angular 1.4)
  • la déclaration de notre template (j’utilise ici un template string ES6). Notez qu’il est possible d’utiliser un template via un fichier html via templateUrl (de la même façon que les directives).
  • Les components utilisent des controller en ‘controller as‘ dont le nom par défaut est $ctrl. Vous pouvez bien entendu le modifier via la propriété controllerAs

Nous voyons que dans le cas de composants d’affichage, les composants sont beaucoup plus simple et concis.

One way data bindings

Une des nouveautés très intéressante de Angular 1.5 est l’apparition du one way binding via la notation ‘<‘. Les changements effectués sur une propriété bindée en one-way ne seront pas répercutés sur l’objet sur lequel il est bindé (sens component -> parent). Les mises à jour sur l’objet source sont répercutées sur le component (parent -> component). On gagne ainsi en performance et en maintenance.

Lifecycle hooks

La version 1.5 introduit également une notion de cycle de vie des components sur lequel nous allons pouvoir nous plugger.
Nous avons les événements suivant sur lesquels nous allons pouvoir nous abonner:

  • onInit : à l’initialisation du component. Idéal pour initialiser notre composants, definir les valeurs par defauts, …
  • onChanges: à chaque modification de valeur d’un binding one-way.
  • onDestroy: lors de la destruction du composant. Idéal pour libérer les ressources …

Tests

Une des choses appréciables avec angular est la testabilité. Les components sont très simple à tester. Les tests d’un controller d’un component se feront via l’utilisation de $componentController inclut dans ngMock. L’avantage est qu’on n’a pas besoin de créer un élément.

Migration d’une directive vers un component

J’ai déjà parlé d’une directive gérant un select avec des valeur numérique. Voyons comment la migrer vers un composant.

Liens

Conclusion

L’arrivée des components dans la version 1.5 d’angular a modifié la façon de développer des applications angular 1.x. Je vous conseille vivement leur utilisation, qui plus est si vous souhaitez préparer la migration vers angular 2.

Angular : Utiliser les classes ES6

J’ai présenté dans un article précédent comment utiliser Babel afin de coder en ES6/ES2015 et transpiler vers ES5.

Angular 2 a été annoncé il y a 1 an à la ng-conf Europe et est aujourd’hui en version bêta. Cette nouvelle version est une réécriture complète qui va changer beaucoup de choses (plus de scope, controlleur, angular.modules, …). La version 1.x d’Angular a 5 ans et beaucoup de choses ont évolués depuis. 3 changements majeurs expliquent ce choix :

Pour info, il sera possible de coder pour la version 2 en ES5, ES6 ou TypeScript (et même dart).

Si vous décider de coder en ES6 ou Typescript, vos services ou vos composants seront des classes ES6. Celles-ci simplifient grandement l’écriture et leur utilisation dans Angular 1.x facilitera la migration vers la version 2.

Voyons maintenant comment utiliser les classes ES6 pour Angular. Les exemples suivants proviennent de mon application de Volley disponible sur github.

Commun

Vos services, controller, directives vont être définis comme des classes et l’injection va se passer par le constructeur. Il va falloir « enregistrer » vos dépendances sur votre objet (sur this) afin de pouvoir les utiliser dans vos méthodes. Attention, on oublie facilement le this !!

Controller

L’utilisation des classes ES6 n’empêchent pas d’utiliser $scope mais la migration en classe est un bon moyen de migrer vers la syntaxe « controller as ». Cette syntaxe n’utilise pas le scope qui n’existe pas en Angular 2.

Avant
Après

Services

Vous savez qu’il y a différent types de service en Angular : services, factory, provider.

Les services de type service sont conçus pour définir un service sous forme de classe (ou comme type instantiable)
Si vous avez des factory, le plus simple est de les convertir en service.

Dans le cas d’un provider, il faut que la classe contienne une propriété nommée $get, qui doit être une fonction qui retourne une factory.

Avant
Après

Directives

Les directives sont les éléments les plus complexes et les plus difficiles à appréhender en Angular 1.x. Attention contrairement aux autres types, il vous faudra instancier vous même vos services.

Avant
Après

Cet exemple n’a aucune dépendance injectée. Si votre directive contient des dépendances il faut les passer en paramètre lors de instanciation de votre directive.


class myDirective {
constructor(userService) {
this.template = `<div>{{fullName}}</div>`;
this.restrict = 'E';
this.scope = {
user: '='
};
this.link = function(scope, element) {
scope.fullName = userService.getFullName(scope.user);
};
}
}
angular.module('myApp').directive('myDirective', ['userService',
(userService) => new myDirective(userService)]);

Conclusion

Les classes ES6/2015 permettent de simplifier le code et facilitera une potentielle migration vers Angular 2.
Cet article n’utilise que les classes ES6 mais il est possible d’utiliser d’autres nouveautés comme les modules ou les décorateurs par exemple. Vous trouverez plus d’informations dans les liens ci dessous.

Liens

Angular 2 :

Angular : Directive avec controller as, bindToController et tests

Une des bonnes pratiques promue par les experts angular est l’utilisation des controller en utilisant la syntaxe ‘controller as’ (des liens sur le sujet en bas de l’article).
En effet, cette pratique permet d’éviter certains écueils lié au prototypage JavaScript et permet de ne plus se coupler au scope.
La notion de scope n’existera plus dans angular 2 (de même que les controller d’ailleurs …) et il est donc conseillé de limiter son utilisation afin de faciliter une migration.

Les équipes d’angular travaillent sur les versions 1.x afin de faciliter la migration et contiennent donc des améliorations pour cela.

La version 1.3 a introduit la propriété bindToController qui permet d’automatiquement de rendre disponible les propriétés déclarés sur le scope dans le controller d’une directive.

La version 1.4 permet de définir directement sur bindToController les propriétés comme on le faisait sur le scope.

Directive avec controller as

Voici un exemple d’une directive tirée de mon application de volley ou l’on déclare une directive avec un controller en utilisant la syntaxe controller as et bindToDirective.

function matchScore() {
  return {
    templateUrl : 'views/directives/match-score.html',
    restrict : 'E',
    scope : true,
    bindToController : {
        match : '='
    },
    controllerAs : 'matchScore',
    controller : function () {
      return {
        scoreSets : function (set, team) {
          return this.match.score[set]
	    .filter(val => val === team)
            .length;
        }
      }
    }
  }
}

angular.module('volleyApp').directive('matchScore', matchScore());

Dans le vue de notre directive, il faut alors utiliser matchScore comme préfixe pour nos bindings.

<div class="list-group-item-text">
    <table class="table table-bordered score">
    <tbody>
    <tr>
        <th>Team</th>
        <th>Set 1</th>
        <th>Set 2</th>
        <th>Set 3</th>
        <th>Set 4</th>
        <th>Set 5</th>
    </tr>
    <tr id="scoreTeam1">
        <th><span id="team1Name">{{matchScore.match.teams.team1}}</span></th>
        <td class="set1"><span>{{matchScore.scoreSets(0, 1)}}</span></td>
        <td class="set2"><span>{{matchScore.scoreSets(1, 1)}}</span></td>
        <td class="set3"><span>{{matchScore.scoreSets(2, 1)}}</span></td>
        <td class="set4"><span>{{matchScore.scoreSets(3, 1)}}</span></td>
        <td class="set5"><span>{{matchScore.scoreSets(4, 1)}}</span></td>
    </tr>
    <tr id="scoreTeam2">
        <th><span id="team1Name"></span>{{matchScore.match.teams.team2}}</th>
        <td class="set1"><span>{{matchScore.scoreSets(0, 2)}}</span></td>
        <td class="set2"><span>{{matchScore.scoreSets(1, 2)}}</span></td>
        <td class="set3"><span>{{matchScore.scoreSets(2, 2)}}</span></td>
        <td class="set4"><span>{{matchScore.scoreSets(3, 2)}}</span></td>
        <td class="set5"><span>{{matchScore.scoreSets(4, 2)}}</span></td>
    </tr>
    </tbody>
</table>

Tests

Si on utilise pas controller as, les méthodes du controller sont accessible via le scope.

Si on utilise controllerAs, nous n’avons plus de scope pour accéder aux méthodes de notre controller. Comment alors tester nos méthodes de controller ?

Angular fournit des méthodes additionnelles à element (élément jQuery/jqLite) et fournit notamment une méthode element.controller qui permet de récupérer un controller. Il faut l’appeler avec le nom de la directive (et non par celui définit via controller as).

//on récupère le controller via element 
var ctrl = element.controller('matchScore');

//On teste nos méthodes
expect(ctrl.scoreSets(0, 1)).toBe(25);

Liens

Protractor : Améliorer vos tests E2E avec le pattern Page Objects

J’ai déjà parlé dans des articles précédents de Protractor (ici et ), l’outil qui permet de faire des tests d’interface, end-to-end (E2E).

Je vais vous présenter ici Page Objects, un design pattern afin de faciliter le développement de vos tests. Page Objects permet :

  • d’avoir un code plus lisible
  • d’éviter la duplication de code
  • d’éviter d’avoir un couplage fort entre votre test et la page à tester
  • de faciliter la maintenance de vos tests

Une page object est une classe qui va servir d’interface entre la page à tester et vos tests. Ainsi, le test n’a pas à interagir avec votre interface mais passe par la classe « Page Object ». Ainsi, si votre interface change, seulement le code de votre page objet sera à modifier et non le test, facilitant ainsi la maintenance. La page object va exposer des méthodes fonctionnelles qui vont rendre plus lisible vos tests.

Exemple

Voici un exemple simple concernant une page de login.

Voici le test sans utilisation de page objects.

describe('Login', function () {

  var ptor, loginInput, passInput, submitBtn, error;

  beforeEach(function () {
    ptor = protractor.getInstance();
    loginInput = by.model('username');
    passInput = by.model('password');
    submitBtn = by.css('[type=submit]');
    error = by.binding('error');
  });

  it('should display error if login password are wrong', function () {
    element(loginInput).sendKeys('wrongUser');
    element(passInput).sendKeys('pass123');
    element(submitBtn).click();

    browser.waitForAngular();

    expect(ptor.getCurrentUrl()).not.toMatch(/\/home/);
    expect(element(error).getText()).not.toEqual('');
  });

  it('should redirect to home page is login/pass are correct',  function () {
    element(loginInput).sendKeys('admin');
    element(passInput).sendKeys('123456');
    element(submitBtn).click();

    browser.waitForAngular();

    expect(ptor.getCurrentUrl()).toMatch(/\/home/);;
  });
});

Voici comment améliorer nos tests avec Page Object. Dans le cas de protractor, les tests sont exécutés via node js. Nous allons créer un module node qui pourra ainsi être chargés dans nos tests.

La page Object de la page de login :

'use strict';

var LoginPage = function () {
  browser.get('http://mySiteUrl');
};

LoginPage.prototype = Object.create({}, {
    loginText: { get: function () { return element(by.model('username')); }},
    passwordText: { get: function () { return element(by.model('password')); }},
    loginButton: { get: function () { return element(by.css('input[type="submit"]')); }},
    error = { get: function () { return element(by.binding('error')).getText(); }},
    login: { value: function (login, password) {
    	this.loginText.sendKeys(login);
    	this.passwordText.sendKeys(password);
    	this.loginButton.click();

        browser.waitForAngular();
  }}
});

module.exports = LoginPage;

Le test devient :

//On charge le module
var LoginPage = require('../pages/login.page.js');

describe('Login', function () {
var page, ptor;

  beforeEach(function () {
    //avant chaque test on on récupère la page
    page = new LoginPage();
    ptor = protractor.getInstance();
  });

  it('should display error if login password are wrong', function () {
     page.login('wrongUser', 'pass');

     expect(ptor.getCurrentUrl()).not.toMatch(/\/home/);
     expect(page.error).not.toEqual('');
  });

  it('should redirect to project home page is login/pass are correct',   function () {
    page.login('julien', 'azerty123');

    expect(ptor.getCurrentUrl()).toMatch(/\/home/);
  });
});

On peut voir que le test est beaucoup plus lisible et maintenable.

De plus, il devient facile de réutiliser notre code. En effet, si on doit tester des pages ou l’on doit être identifié, on peut facilement réutiliser la page objects de login afin de se connecter avant chaque test.

J’ai mis à jour les tests E2E de mon application de volley. Le code est disponible sur github.

Références

Bon tests 😉

AngularJS : Lancer protractor avec Grunt

Dans un article précédent, je vous ai montré comment faire des tests end to end (E2E) avec Protractor pour tester une application Angular. Voyons comment nous pouvons utiliser grunt afin de faciliter le lancement de ces tests.

Nous allons avoir besoin des modules grunt à installer avec la commande :

npm install grunt-protractor-runner grunt-shell grunt-protractor-webdriver --save-dev
  • grunt-protractor-runner : permet de lancer protractor
  • grunt-shell : permet de lancer des commandes shells. Dans notre cas, il permet de mettre à jour webdriver dans la dernière version
  • grunt-protractor-webdriver : permet de lancer webdriver

Il faut ensuite modifier le fichier Gruntfile.js afin de configurer les plugins.

shell: {
  updateSeleniumWebDriver: {
    command: 'node node_modules/protractor/bin/webdriver-manager update',
      options: {
        stdout: true
      }
    }
  },

  'protractor_webdriver': {
     options: {
       // Task-specific options go here.
     },
     run: {
       // Target-specific file lists and/or options go here.
     }
  },

  protractor: {
    options: {
      configFile: 'node_modules/protractor/referenceConf.js', // Default config file
      keepAlive: true, // If false, the grunt process stops when the test fails.
      noColor: false, // If true, protractor will not use colors in its output.
      args: {
        // Arguments passed to the command
      }
    },
    local: {
      options: {
        configFile: 'protractor.conf.js', // Target-specific config file
           args: {} // Target-specific arguments
        }
      }
    }
  }

  ///autre configs ...

On créé ensuite une tache qui permet de lancer les tests E2E.

grunt.registerTask('e2e', [
    'connect:dist',
    'shell:updateSeleniumWebDriver',
    'protractor_webdriver:run',
    'protractor:local'
  ]);

La tache effectue les actions suivantes :

  • on lance le site avec connect
  • on met à jour selenium web driver
  • on lance webdriver
  • on lance les tests

Le lancement des tests se fait ensuite via la commande :

grunt e2e

Voici le résultat de l’exécution des tests :
Protractor lancé via grunt

Comme d’habitude, je l’ai mis en place sur mon application de Volley dont vous pouvez trouver le code sur github.

Pour ceux qui utilisent gulp, il existe les modules également :
gulp-protractror,
gulp-shell

AngularJs : Tester une directive avec un templateUrl

En AngularJS, lorsque le template d’un directive est complexe, une bonne pratique est de mettre ce template dans un fichier html séparé. Cela facilite la lecture de ce dernier et évite d’avoir à faire de la concaténation de chaines …

Cependant, lors de nos tests, le template n’est pas chargé et nous obtenons l’erreur suivante indiquant qu’une requête inattendue vers notre template à été effectuée.

Angular-Error-templateUrl

Voyons comment remédier à ce problème.

Il faut savoir que les templates sont ne sont chargés qu’une seule fois avant d’être mis dans un cache : le $templateCache. A la 2eme utilisation, le template sera récupérer dans le cache. Angular permet d’interagir avec ce templateCache. Ainsi, la méthode get permet de récuperer un template et put d’en rajouter un.

Nous allons pouvoir utiliser ce $templateCache dans nos tests afin d’y placer nos templates et pouvoir les utiliser lors de l’exécution des tests sans faire de requêtes http.

Nous allons utiliser pour le mettre en place le plugin grunt karma-ng-html2js-preprocessor. Ce dernier compile les template html en js, les enregistre dans le $templateCache et les enregistre dans un module qui nous permet de récuperer notre template.

L’installation du plugin se fait via la commande :

npm install karma-ng-html2js-preprocessor --save--dev

Il faut ensuite charger et configurer le plugin.

Voici le fichier karma.conf.js avec la configuration liée à karma-ng-html2js-preprocessor.


files: [
      //load js files ...
      
      //load directives templates
      'app/scripts/directives/templates/*.html'
    ],

// Plugins
plugins: [ 
 'karma-jasmine',
 'karma-chrome-launcher',
 'karma-firefox-launcher',
 'karma-coverage',
 'karma-ng-html2js-preprocessor'
],

ngHtml2JsPreprocessor: {
  // strip this from the file path
  stripPrefix: 'app/'
},

preprocessors: {
 'app/scripts/directives/templates/*.html': 'ng-html2js'
},

Voici les choses à faire :

  • Ajouter dans la liste des fichiers (files) vos templates html
  • Ajouter karma-ng-html2js-preprocessor à la liste des plugins à charger
  • Configurer le plugin dans la partie ngHtml2JsPreprocessor. (Dans mon cas, il suffit de dire qu’on ignore le prefix app/ dans le chemin des fichiers).
  • On configure ng-html2js dans la partie preprocessors afin de lui dire quels fichiers il doit traiter

Voici le test d’un directive chargeant un template html.

'use strict';

describe('Directive: numselect', function () {

  // load the directive's module
  beforeEach(module('volleyApp'));
  // load the template
  beforeEach(module('views/directives/num-select.html'));

  var element,
    scope;

  beforeEach(inject(function ($rootScope) {
    scope = $rootScope.$new();
    scope.options = 5;
  }));

  it('should create select with 4 options', inject(function ($compile) {
    element = angular.element('<numselect nb-options="options"></numselect>');
    element = $compile(element)(scope);

    scope.$digest();

    expect(element.find('select').length).toBe(1);
    expect(element.isolateScope().num.length).toBe(6);
  }));

  ///autres tests
});

Vous pouvez voir qu’on charge le template en tant que module.

J’ai mis en place ce mécanisme sur mon application de Volley. Voici le lien vers

Angular : Les propriétés spéciales dans un ngRepeat

Dans une boucle ngRepeat, chaque item à son propre scope. Les propriétés spéciales suivantes sont ajoutées dans le scope de ces derniers :

  • $index : la position de l’item (commence à 0)
  • $first : indique si c’est le premier élément
  • $middle : indique si c’est un élément entre le premier et le dernier
  • $last : indique si c’est le dernier élément
  • $even : indique si c’est un élément pair
  • $odd : indique si c’est un élément impair

On peut ainsi binder sur ces propriétés pour facilement afficher un index, affecter des classes CSS via ngClass …

Dans l’exemple suivant, on affiche l’index de l’élément (oui j’aurai pu utiliser une liste ordonnée 🙂 mais c’est pour la démo …) et on affecte une classe si l’élément est pair et n’est ni le premier ni le dernier.

La documentation officielle de ngRepeat

Angular : Test End to End avec Protractor

Je vous ai déjà parlé des tests unitaires sur un projet Angular. Aujourd’hui je vais vous présenter comment faire des tests d’intégration (end to end ou E2E). Le but des tests d’intégration est de s’assurer que tous les composants de votre application fonctionnent correctement ensemble. Pour cela, les tests seront joués dans un navigateur et des actions utilisateurs seront simulées.
Les tests d’intégration sont donc complémentaire des tests unitaires.

Angular a été pensé pour être testable et il y a donc un outil pour les tests E2E : Protractor, développé par la team Angular (Il est possible de l’utiliser pour tester des applications qui ne sont pas basé sur Angular).
Protractor utilise Selenium qui permet de contrôler un navigateur (aller à une page, cliquer sur un bouton, …)

Installation

Protractor est un module node et il faut donc avoir node installé sur son poste.

Je vous conseille de l’installer en global (Préfixer la commande par sudo si vous êtes sous Linux (Ubuntu pour moi).

npm install protractor -g

Il faut ensuite configurer selenium via la commande suivante

webdriver-manager update

Configuration

La configuration de Protractor se fait via un fichier .js.
Voici celui que j’utilise dans mon application de volley :

Ecriture des tests

Il faut ensuite écrire vos tests. Par défaut, protractor utilise Jasmine mais vous pouvez utiliser d’autres framework de test comme mocha par exemple.

Protractor met à votre disposition des variables globales vous permettant d’interagir avec le navigateur comme par exemple :

  • browser: browser.get(‘url’) : permet d’ouvrir la page passé en paramètre
  • element and by: element(by.model(‘yourName’)) : permet de récuperer un élement de la apge
  • protractor: protractor.Key : permet la gestion des touches (ex : protractor.Key.ENTER)

Protractor étant à la base pensé pour angular, il fournit des sélecteurs lié à angular comme

  • by.model(‘first’) : récupère l’input lié à la variable de scope first (ng-model= »first »)
  • by.binding(‘myModel’) : récupere l’élement bindé sur myModel (
  • by.repeater(‘user in users’).row(0).column(‘name’) : récupère le nom d’un 1er element de la liste users

Voici une exemple de test sur mon application de volley qui permet de vérifier qu’on ne peut pas lancer de partie si le nom d’une des équipes n’est pas renseignée :

Voici le lien vers la documentation officielle de l’api de protractor ainsi qu’une CheatSheet et un gist (en coffeescript).

Dans le cas ou vous faites un appel serveur, il faut utiliser la ligne suivante afin de ne pas poursuivre vos tests et notamment vos vérifications tant qu’angular n’a pas traité le retour

browser.waitForAngular();

Lancer les tests

Il faut d’abord lancer le server Selenium (webdriver) via la commande suivante :

webdriver-manager start

On peut alors lancer dans un autre terminal protractor :

protractor test/e2e/config.js // Chemin vers votre fichier de configuration

Une fenêtre de navigateur va alors se lancer et jouer les tests sous vos yeux. Magique !

Le résultat de vos tests sera affiché dans la console :

Protractor : résultats des tests E2E

Comme vous pouvez le voir, il me reste encore du travail pour tout valider 😉 .

Attention, votre site doit être accessible. Dans mon cas, je le lance via la commande

grunt serve

NB : Il existe un plugin pour lancer protractor depuis grunt : grunt-protractor-runner. Je n’ai pas encore eu le temps de le tester …

Ressources

AngularJs : Communication entre une directive et un controller

Un petit article rapide concernant la communication entre une directive et un controller en Angular via une fonction.

Il est possible de définir dans une directive une fonction de callback vers notre controller. Il faut définir une propriété de type fonction via un &.

scope: {
   demoOnClick: '&'
}

Voyons comment utiliser une fonction avec des paramètres.

Appel de fonction avec paramètre

Angular utilise l’injection de dépendance qui permet d’injecter dans les différents composants Angular, les ressources requises par ce dernier.

Cela fonctionne de la même façon lors de l’appel de la fonction.

Lors de l’appel de la fonction coté directive, il faut passer un objet JavaScript avec les paramètres nommés. Dans votre code HTML où vous utilisez la directive, il faut spécifier pour la variable de type fonction, le nom des paramètres que l’on souhaite recevoir. Ainsi Angular va injecter seulement les paramètres dont vous avez besoin. Attention de bien utiliser les noms des propriétés au risque de recevoir des undefined ….

Voici un exemple :

Cela est déconcertant au début et j’avoue avoir perdu un peu de temps. Cela peut s’avérer pratique dans le cas d’une directive qui peut passer plusieurs paramètres et dont votre controller n’a besoin que d’un paramètre spécifique comme dans l’exemple ci dessus. Ainsi, il ne n’est pas nécessaire de définir tout les paramètres et ainsi éviter des erreurs JSHint nous indiquant qu’un paramètre n’est pas utilisé.