Aurelia : Tester ses Custom Elements

Tester un service est assez simple. Il suffit de l'importer et de l'instancier en lui passant des mocks de dépendances. Par contre, pour les Custom Elements, c'est plus compliqué car il faut le faire passer à travers le moteur de Aurelia. Pour cela, nous avons donc le module aurelia-testing, inclus par défaut dans un projet créé avec aurelia-cli. Voyons comment cela fonctionne.

(Attention, si vous voulez aussi utiliser async/await dans vos tests, il vous faudra modifier tests/unit/setup.js pour y importer regenerator-runtime. Mais vous le savez normalement si vous utilisez déjà async/await dans votre app)

Découverte

Prenons un custom element tout simple :

src/my-element.js

export class MyElement {}  

src/my-element.html

<template>  
<p>Hello World</p>  
</template>  

Voici le test :

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView('<my-element><my-element>');
  });

  afterEach(() => {
    component.dispose();
  });

  it('should render template', async done => {
    await component.create(bootstrap);
    const element = document.querySelector('my-element');
    expect(element.innerText.trim()).toBe('Hello World');
  });
});

StageComponent va donc nous servir à rendre le composant dans le navigateur après l'avoir configuré comme on en a besoin. Ici, notre composant est très simple et n'a pas de configuration particulière. Nous nous contentons de vérifier que l'élément dans le DOM contient bien le texte spécifié dans le template.

Maintenant, essayons avec des données dynamiques

Attributs

src/my-element.js

import {bindable} from 'aurelia-framework';  
export class MyElement {  
  @bindable foo;
}

src/my-element.html

<template>  
<p>${foo}</p>  
</template>  

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element
          foo.bind="foo"><my-element>
      `)
      .boundTo({
        foo: 'foo'
      });
  });

  afterEach(() => {
    component.dispose();
  });

  it('should render foo', async done => {
    await component.create(bootstrap);
    const element = document.querySelector('my-element');
    expect(element.innerText.trim()).toBe('foo');
  });
});

Si on veut pouvoir modifier la valeur des propriétés, il va falloir passer en mode manuel grâce à manuallyHandleLifecycle et appeler explicitement chaque étape du cycle de vie du composant:

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element
          foo.bind="foo"><my-element>
      `)
      .boundTo({
        foo: 'foo'
      });
  });

  afterEach(() => {
    component.dispose();
  });

  it('should render foo and changes value', async done => {
    await component.manuallyHandleLifecycle().create(bootstrap);
    await component.bind();
    await component.attached();
    const element = document.querySelector('my-element');
    expect(element.innerText.trim()).toBe('foo');

    await component.bind({
      foo: 'bar'
    });
    expect(element.innerText.trim()).toBe('bar');
  });
});

On peut aussi lire des propriétés du viewModel ou appeler des méthodes qui lui sont attachées via component.viewModel :

src/my-element.js

import {bindable} from 'aurelia-framework';  
export class MyElement {  
  @bindable onSelect;

  select(item) {
    if (typeof this.onSelect === 'function') {
      this.onSelect(item)
    }
  }
}

src/my-element.html

<template>  
<ul>  
  <li
    click.delegate="select(1)">1</li>
  <li
    click.delegate="select(2)">2</li>
  <li
    click.delegate="select(3)">3</li>
</ul>  
</template>  

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element
          on-select.bind="select"><my-element>
      `);
  });

  afterEach(() => {
    component.dispose();
  });

  it('should select an item', async done => {
    let expected;
    component.boundTo({
      select(e) {
        expected = e;
      }
    })
    await component.create(bootstrap);
    component.viewModel.select(42);
    expect(expected).toBe(42);
  });
});

Mais dans ce cas là, on peut aussi vouloir tester si la fonction est correctement exécutée lors du click sur le bon élément. On va donc rédiger notre test ainsi :

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element
          on-select.bind="select"><my-element>
      `);
  });

  afterEach(() => {
    component.dispose();
  });

  it('should select an item on click', async done => {
    let expected;
    component.boundTo({
      select(e) {
        expected = e;
      }
    })
    await component.create(bootstrap);
    const lis = component.element.querySelectorAll('li');
    lis[0].click();
    expect(expected).toBe(1);
    lis[1].click();
    expect(expected).toBe(2);
    lis[2].click();
    expect(expected).toBe(3);
  });
});

Slot

On peut de la même façon vérifier que les slots sont correctement remplis :

src/my-element.js

export class MyElement {}  

src/my-element.html

<template>  
<header  
  class="header">
  <slot
    name="header"></slot>
</header>  
<div  
  class="content">
  <slot
    name="content"></slot>
</div>  
</template>  

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element>
          <h1
            slot="header">Title</h1>
          <p
            slot="content">Hello World"</p>
        <my-element>
      `);
  });

  afterEach(() => {
    component.dispose();
  });

  it('should render title and content slots', async done => {
    await component.create(bootstrap);
    const element = document.querySelector('my-element');
    expect(element.querySelector('.header').innerText.trim()).toBe('Title');
    expect(element.querySelector('.content').innerText.trim()).toBe('Hello World');
  });
});

Configuration

Il est parfois nécessaire de configurer son environnement différemment pour un test donné. Pour instancier un plugin nécessaire au fonctionnement du custom element, ou pour enregistrer un mock de dépendance dans l'injecteur. Nous allons gérer tout ça grâce à la méthode configure de StageComponent :

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element') // Path related to src
      .inView(`
        <my-element
          on-select.bind="select"><my-element>
      `);
    component.configure = aurelia => {
      aurelia.use
        .standardConfiguration()
        .plugin('aurelia-property-injection');
    };
  });
});

Ici, notre custom element fait usage de la lib aurelia-property-injection et il faut donc le charger au préalable.

Si on a besoin de remplacer une dépendance par un mock, on peut faire ça aussi dans la configuration :
src/my-element.js

import {inject} from 'aurelia-framework';  
import {MyService} from './my-service';

@inject(MyService)
export class MyElement {  
  constructor(myService) {
    this.service = myService;
  }

  get something() {
    return this.service.getSomething();
  }
}

src/my-element.html

<template>  
<p>${something}</p>  
</template>  

tests/my-element.spec.js

import {StageComponent} from 'aurelia-testing';  
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyElement', () => {  
  let component;
  const MyServiceMocked = {
    getSomething: jasmine.createSpy('getSomething').and.returnValue('something')
  };

  beforeEach(() => {
    component = StageComponent
      .withResources('my-element')
      .inView(`
        <my-element><my-element>
      `);
    component.configure = aurelia => {
      aurelia.container.registerInstance('MyService', MyServiceMocked);
    };
  });

  afterEach(() => {
    component.dispose();
  });

  it('should render something', async done => {
    await component.create(bootstrap);
    expect(document.querySelector('p').innerText.trim()).toBe('something');
    done();
  });
});

Tout cela suffit déjà à tester la grande majorité des cas de figure. Nous verrons dans un prochain article comment teste les custom attributes.

Hadrien

Hi, I'm a french Web Lead Developer, Front End Architect from Toulouse, France. I've worked for 7 years for Overblog then 2 years with AngularJS. Now, I'm a great fan of Aurelia.io.

Toulouse, France https://hadrien.eu