Testing Stenciljs Components

Update: The newer version of this article is here. Changes made in Stencil make this article mostly irrelevant.

Note: Tests were adopted to changes performed in Stencil 0.7.19

Testing stenciljs is still in its infancy, and I found myself collecting some best practices and code examples on testing the component. The first batch is here:

The application tested is a Todo app, similar to the ones on the Todomvc website. The application is based on the stencil-app-starter and has 3 components:

  • my-app: App main component
  • todo-list: manages the list of todo items
  • todo-item: A single todo item editing, removing and completion.

Describe Component’s Instance Tests

The first method to test components is by creating an instance of the component class:

import { TodoList } from './todo-list';
let comp = new TodoList();

It should test complex logic *

( *The logic of getRemainingItems is not really complex)

This is a great way to test that the logic of a method of your class is done correctly with a large number of variances. Don’t just go the happy path! add few edge cases to see you are handling those.

import { TodoList } from './todo-list';
const ITEMS = [
{ name: 'an item', done: false},
{ name: 'an item', done: false},
{ name: 'an item', done: true}
];
it('should display remaining items', () => {
let comp = new TodoList();
comp.items =ITEMS;
expect(comp.getRemainingItems()).toEqual(2);
});

It should test events and payloads

You can test that events are fired with correct payload by spying on the class events emitting and verifying the received payload:

it('should remove item from list on remove', () => {
let comp = new TodoList();
comp.items = ITEMS;
const changedSpy = jest.fn();
comp.todoItemsChanged = {
emit: changedSpy
};
//const changedSpy = jest.spyOn(comp.todoItemsChanged, 'emit');
comp.removeItem(1);
expect(changedSpy).toHaveBeenCalledWith([
{ name: 'Item 1', done: false},
{ name: 'Item 3', done: true}
]);


});

In most cases these tests better occur on rendered components, but if you have (again) relatively complex rendering logic, you can test it faster with component instance.

Describe Rendered Component Tests

The second method for testing (and where most tests should occur) is by rendering a component and performing action on it. The first thing is to define a test window to be used. The window is (as of 0.7.25) a jsdom window:

let win;
beforeEach(() => {
win = new TestWindow();
});

It should test Components Rendering

Use Jest snapshots to render the items with different parameters. The following test will only render the TodoList component (shallow).

it('should render with items list', async () => {
let element = await win.load({
components: [TodoList],
html: '<todo-list></todo-list>'
});
element.items = [
{name: 'Milk the cow', done: true},
{name: 'Buy milk', done: false}
];
win.flush();
expect(element).toMatchSnapshot();
});

You can also test integration between components by deep rendering components:

it('should do deep rendering', async () => {
let element = await win.load({
components: [TodoList, TodoItem],
html: '<todo-list></todo-list>'
});
element.items = ITEMS;
win.flush();
expect(element).toMatchSnapshot();
});

It should test user events

When certain user interactions occur (click, resize etc.), an event may be fired up. The tests should catch the events and evaluate the received payload to be the correct one after the interaction occurred. The following is an example of testing that the event was raised when a todo item was toggled:

it('should emit item toggled', async () => {
let eventSpy = jest.fn();
win.document.addEventListener('itemToggled', eventSpy);
element.querySelector('.toggle').click();
expect(eventSpy).toHaveBeenCalled();
});

Or we can use it in integration test with the parent component. In this tests we verify that todo-list has emitted an event with the item changed:

it('should toggle second item', async () => {
let eventSpy = jest.fn();
win.document.addEventListener('todoItemsChanged', eventSpy);
let toggles = element.querySelectorAll('.toggle');
toggles[1].click();
win.flush();
expect(eventSpy).toHaveBeenCalled();
// event called data is in the mock.calls of the jest function
expect(eventSpy.mock.calls[0][0].detail[2].done).toEqual(true);
});

It should test user DOM events

User DOM events, such as change of an input are not triggered automatically with jsdom. The todo-item uses the <input onChange={ev => triggeredFunction(ev)} . To trigger the onChange during test we need to create the event and dispatch it on the input element:

it('should emit item changing', () => {            
// create a spy on the event and attach to listener
let eventSpy = jest.fn();
win.document.addEventListener('itemChanged', eventSpy);

// Click on the label to enable input
element.querySelector('label').click();
win.flush();

// create the change event
let change = win.document.createEvent('Event');
change.initEvent('change', true, false);
// Set value on the input element & dispatch the change event
let value = 'New Value';
let input = element.querySelector('.edit');
input.value = value;
input.dispatchEvent(change);

// expectations
expect(eventSpy).toHaveBeenCalled();
expect(eventSpy.mock.calls[0][0].detail).toEqual(value);
});

You can also expand this test to be an integration test with the parent todo-list:

it('should update second item', async () => {
let eventSpy = jest.fn();
win.document.addEventListener('todoItemsChanged', eventSpy);
let todo = element.querySelectorAll('todo-item')[1];
todo.querySelector('label').click();
win.flush();

// create an input event
let change = win.document.createEvent('Event');
change.initEvent('change', true, false);

// Set value on the input element & dispatch the event
let value = 'New Value';
let input = element.querySelector('.edit');
input.value = value;
input.dispatchEvent(change);
win.flush();

// expectations
expect(eventSpy).toHaveBeenCalled();
expect(eventSpy.mock.calls[0][0].detail[1].name).toEqual(value);
});

The full repo is here.

Happy testing and share your best practices.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tally Barak

Tally Barak

151 Followers

If you can’t explain it simply, you don’t understand it well enough.” — Einstein