Testing Phoenix LiveView hooks with Jest

Phoenix LiveView allows for JavaScript Hooks to extend live view pages with small powerful bits of JavaScript. This post showcases how one can test these hooks with the power of Jest.

Although one ususally writes more Elixir code than Javascript when using Phoenix LiveView, sometimes there are just things that can only be solved by adding some lines of JS into the mix. In LiveView these can be accomplished by writing little hooks.

LiveView ships with good options to test the Elixir code paths in isolation. For Javascript there is currently no support built into LiveView 🔥. But with some little test helpers you can easily test your code in Jest (or probably other JS unit test runners when including jsdom yourself) .

In order for your hooks to be testable, it is nice when they are bundled separately in their own ES6 modules.

// app.js
import LiveSocket from 'phoenix_live_view';
import Hooks from './hooks/all';

const liveSocket = new LiveSocket('/live', Socket, { hooks: Hooks });
liveSocket.connect();

// hooks/all.js
import MyHook from './my_hook';

const Hooks = {
  MyHook,
  // more hooks
};

export default Hooks;

To showcase how to test a hook we use this simple LiveView hook that replaces the text content of an element when the element is first mounted to the screen.

// hooks/my_hook.js
const MyHook = {
  mounted() {
    this.element.textContent = "New content";
  },
};

export default MyHook;

Next we need to install jest into our package.json with npm install jest and add some default default config for it.

// jest.config.js
module.exports = {
  testPathIgnorePatterns: ['/node_modules/'],
};

// package.json
{
  "scripts": {
    "test": "jest"
  },
  // ...
}

When LiveView hooks are mounted, they are inserted as callbacks into a ViewHook class. You can check out the source code in the live view repository. All that we have to do, is to provide a similar environment:

// live_view_test.js
class ViewHookTest {
  constructor(hook, element) {
    this.el = element;
    this.__callbacks = hook;
    for(let key in this.__callbacks){ this[key] = this.__callbacks[key] }
  }

  trigger(callbackName) {
    this.__callbacks[callbackName].bind(this)();
  }

  pushEvent(_event, _payload) {}
  pushEvent(_target, _event, _payload) {}

  element() {
    return this.el;
  }
}

function createElementFromHTML(htmlString) {
  const div = document.createElement('div');
  div.innerHTML = htmlString.trim();
  return div.firstChild;
}

function renderHook(htmlString, hook) {
  const element = createElementFromHTML(htmlString);
  return new ViewHookTest(hook, element);
}

export { renderHook, HookTest, createElementFromHTML };

With the power of Jest’s included jsdom and our little HookTest module we can now render our hook. To do this, we simulate a DOM element that implements our hook.

// hooks/my_hook.js
import MyHook from './my_hook';
import { renderHook } from '../live_view_test';

describe('MyHook', () => {
  it('repaces the textContent', () => {
    const hook = renderHook('<div>Old content</div>', MyHook);
    hook.trigger('mounted');
    expect(hook.element().textContent).toEqual('New content');
  });
});

And now the tests can be run with npm run test. 🎉🌈🦄

With the two helper functions trigger and element you can trigger your hooks and get a reference to the DOM element under test. For testing things around the pushEvent API, you can use Jest’s spyOn feature. For more information refer to the docs of jest, they are well written and awesome.

Happy testing 🌴

© 2020 bitcrowd GmbH.