Skip to content
This repository was archived by the owner on Mar 7, 2023. It is now read-only.
David Boureau edited this page Mar 9, 2017 · 33 revisions

Pix-Live

Framework de tests

Le framework de test par défaut proposé par Ember.js est QUnit.

Nous estimons que ce framework est aujourd'hui dépassé et en retard en termes de communauté, fonctionnalités, intégrations support et évolution, par rapport à d'autres frameworks tels que Jasmine ou Mocha. C'est pourquoi nous avons fait le choix de le remplacer par ce dernier, considéré comme la référence dans l'écosystème JS depuis plusieurs années (un exploit dans le monde JS!).

Le remplacement de QUnit par Mocha s'est faite grâce au plugin ember-cli-mocha.

Exécuter les tests

L'exécution des tests se fait via la commande ember test (ou ember t). Il est possible de rejouer automatiquement les tests à chaque changement via la commande ember test --serve.

Il est possible de jouer directement les tests (sans passer par ember test) lorsque l'application est lancée (via ember serve ou ember s). Pour ce faire, il faut accéder à l'URL http://localhost:4200/tests.

Pour lancer un sous-ensemble défini de tests, on peut utiliser l'option --filter, par exemple : ember test --filter="assessments".

Le filtrage peut se faire aussi directement au niveau des tests (plutôt que du CLI), grâce aux options Mocha skip et only.

Typologies de tests

Ember.js permet de concevoir 3 types de tests :

  • unitaires : pour tout ce qui est computed properties, serializers, adapters, helpers, services
  • intégration : pour vérifier le rendu et le comportement d'un composant
  • acceptance : pour jouer des scénarios end-to-end (E2E) dans des conditions au plus proches du réel

Tests unitaires

import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupTest } from 'ember-mocha';

describe('Unit | Component | feedback-panel', function () {

  setupTest('component:feedback-panel', {});

  describe('#isFormClosed', function () {

    it('should return true by default', function () {
      // given
      const component = this.subject();
      // when
      const isFormClosed = component.get('isFormClosed');
      // then
      expect(isFormClosed).to.be.true;
    });

    it('should return true if status equals "FORM_CLOSED"', function () {
      // given
      const component = this.subject();
      component.set('status', 'FORM_CLOSED');
      // when
      const isFormClosed = component.get('isFormClosed');
      // then
      expect(isFormClosed).to.be.true;
    });

    it('should return false if status is not equal to "FORM_CLOSED"', function () {
      // given
      const component = this.subject();
      component.set('status', 'FORM_OPENED');
      // when
      const isFormClosed = component.get('isFormClosed');
      // then
      expect(isFormClosed).to.be.false;
    });
  });
});

Tests d'intégration

import Ember from 'ember';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupComponentTest } from 'ember-mocha';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';

// Définition des classes CSS et autres constantes utilisées plusieurs fois
const LINK_VIEW = '.feedback-panel__view--link';
const FORM_VIEW = '.feedback-panel__view--form';
const MERCIX_VIEW = '.feedback-panel__view--mercix';
const OPEN_LINK = '.feedback-panel__open-link';
const BUTTON_SEND = '.feedback-panel__button--send';
const BUTTON_CANCEL = '.feedback-panel__button--cancel';

// Définition des fonctions utils
function expectLinkViewToBeVisible(component) {
  expect(component.$(LINK_VIEW)).to.have.length(1);
  expect(component.$(FORM_VIEW)).to.have.length(0);
  expect(component.$(MERCIX_VIEW)).to.have.length(0);
}

function expectFormViewToBeVisible(component) {
  expect(component.$(LINK_VIEW)).to.have.length(0);
  expect(component.$(FORM_VIEW)).to.have.length(1);
  expect(component.$(MERCIX_VIEW)).to.have.length(0);
}

function expectMercixViewToBeVisible(component) {
  expect(component.$(LINK_VIEW)).to.have.length(0);
  expect(component.$(FORM_VIEW)).to.have.length(0);
  expect(component.$(MERCIX_VIEW)).to.have.length(1);
}

describe('Integration | Component | feedback-panel', function () {

  setupComponentTest('feedback-panel', {
    integration: true
  });

  describe('Default rendering', function () {

    it('should display only the "link" view', function () {
      // when
      this.render(hbs`{{feedback-panel}}`);
      // then
      expectLinkViewToBeVisible(this);
    });
  });

  describe('Link view', function () {

    beforeEach(function () {
      this.render(hbs`{{feedback-panel status='FORM_CLOSED'}}`);
    });

    it('should display only the "link" view', function () {
      expectLinkViewToBeVisible(this);
    });

    it('the link label should be "Signaler un problème"', function () {
      expect(this.$(OPEN_LINK).text()).to.contains('Signaler un problème');
    });

    it('clicking on the open link should hide the "link" view and display the "form" view', function () {
      // when
      this.$(OPEN_LINK).click();
      // then
      expectFormViewToBeVisible(this);
    });
  });

  describe('Form view', function () {

    // Exemple de stubbing de la méthode Ember.Model#save()
    let isSaveMethodCalled = false;
    const storeStub = Ember.Service.extend({
      createRecord() {
        return Object.create({
          save() {
            isSaveMethodCalled = true;
            return Ember.RSVP.resolve();
          }
        });
      }
    });

    beforeEach(function () {
      // configure answer & cie. model object
      const assessment = Ember.Object.extend({ id: 'assessment_id' }).create();
      const challenge = Ember.Object.extend({ id: 'challenge_id' }).create();
      const answer = Ember.Object.extend({ id: 'answer_id', assessment, challenge }).create();

      // render component
      this.set('answer', answer);
      this.render(hbs`{{feedback-panel answer=answer status='FORM_OPENED'}}`);

      // stub store service
      this.register('service:store', storeStub);
      this.inject.service('store', { as: 'store' });
      isSaveMethodCalled = false;
    });

    it('should display only the "form" view', function () {
      expectFormViewToBeVisible(this);
    });

    it('should contain email input field', function () {
      const $email = this.$('input.feedback-panel__field--email');
      expect($email).to.have.length(1);
      expect($email.attr('placeholder')).to.equal('Votre email (optionnel)');
    });

    it('should contain content textarea field', function () {
      const $password = this.$('textarea.feedback-panel__field--content');
      expect($password).to.have.length(1);
      expect($password.attr('placeholder')).to.equal('Votre message');
    });

    it('should contain "send" button with label "Envoyer" and placeholder "Votre email (optionnel)"', function () {
      const $buttonSend = this.$(BUTTON_SEND);
      expect($buttonSend).to.have.length(1);
      expect($buttonSend.text()).to.equal('Envoyer');
    });

    it('should contain "cancel" button with label "Annuler" and placeholder "Votre message"', function () {
      const $buttonCancel = this.$(BUTTON_CANCEL);
      expect($buttonCancel).to.have.length(1);
      expect($buttonCancel.text()).to.equal('Annuler');
    });

    it('clicking on "cancel" button should close the "form" view and and display the "link" view', function () {
      // when
      this.$(BUTTON_CANCEL).click();
      // then
      expectLinkViewToBeVisible(this);
    });

    it('clicking on "send" button should save the feedback into the store / API and display the "mercix" view', function () {
      // given
      const $content = this.$('.feedback-panel__field--content');
      $content.val('Prêtes-moi ta plume, pour écrire un mot');
      $content.change();
      // when
      this.$(BUTTON_SEND).click();
      // then
      return wait().then(() => {
        expect(isSaveMethodCalled).to.be.true;
        expectMercixViewToBeVisible(this);
      });
    });
  });

  describe('Mercix view', function () {

    beforeEach(function () {
      this.render(hbs`{{feedback-panel status='FORM_SUBMITTED'}}`);
    });

    it('should display only the "mercix" view', function () {
      expectMercixViewToBeVisible(this);
    });
  });

  describe('Error management', function () {

    it('should display error if "content" is blank', function () {
      // given
      this.render(hbs`{{feedback-panel status='FORM_OPENED' content='   '}}`);

      // when
      this.$(BUTTON_SEND).click();

      // then
      expect(this.$('.alert')).to.have.length(1);
      expectFormViewToBeVisible(this);
    });

    it('should display error if "email" is set but invalid', function () {
      // given
      this.render(hbs`{{feedback-panel status='FORM_OPENED' content='Lorem ipsum dolor sit amet' email='wrong_email'}}`);

      // when
      this.$(BUTTON_SEND).click();

      expect(this.$('.alert')).to.have.length(1);
      expectFormViewToBeVisible(this);
    });

    it('should not display error if "form" view (with error) was closed and re-opened', function () {
      // given
      this.render(hbs`{{feedback-panel status='FORM_OPENED' content='   '}}`);
      this.$(BUTTON_SEND).click();
      expect(this.$('.alert')).to.have.length(1);

      // when
      this.$(BUTTON_CANCEL).click();
      this.$(OPEN_LINK).click();

      // then
      expect(this.$('.alert')).to.have.length(0);
    });
  });
});

Tests d'acceptation

/* On déclare explicitement les imports (même pour Mocha) pour faciliter la vie des IDE / Linters */
import { describe, it, beforeEach, afterEach } from 'mocha';
import { expect } from 'chai';
import startApp from '../helpers/start-app';
import destroyApp from '../helpers/destroy-app';

/* Définition des méthodes utiles, si besoin */
function assertDisplayedUrl(actualUrl, expectedUrl) {
  expect(actualUrl).to.equal(expectedUrl);
}

/* Définition des constantes, si besoin */
const INDEX_PAGE_URL = '/';
const INDEX_LANDING_TEXT_SELECTOR = '.first-page-hero__main-value-prop';

/* Description de la suite de tests */
describe('Acceptance | a1 - Accéder à la plateforme pour démarrer un test', function () {

  /* Déclaration de l'instance de l'application qui sera lancée lors des tests */ 
  let application;

  /* Initialisation de l'application, de préférence dans un forEach pour avoir des tests le plus indépendants */
  beforeEach(function () {
    application = startApp();
  });

  /* Destruction de l'application */
  afterEach(function () {
    destroyApp(application);
  });

  /* Toutes les méthodes asynchrones de type ember-helper peuvent être jouées d'affilées dans un beforeEach */
  beforeEach(function () {
    visit(INDEX_PAGE_URL);
    click(INDEX_LANDING_TEXT_SELECTOR);
  });

  it('a1.0 peut visiter /', function () {
    assertDisplayedUrl(currentURL(), INDEX_PAGE_URL) {
  });

  it('a1.1 la landing page contient un pitch de présentation', function () {
    const $landingText = findWithAssert(INDEX_LANDING_TEXT_SELECTOR).text();
    expect($landingText).to.contains('Développez vos compétences numériques');
  });

  it('a1.2 Sur la landing page, un lien pointant vers la page projet est présent dans les valeurs pix', function(){
    findWithAssert('.first-page-about a[href="/projet"]');
  });

});

Pix-API

Clone this wiki locally