Testing your Solid.js code with jest
Tags: solidjs, webdev, testing
Published: 2021-10-14 20:53:40

![Solid.js logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fla7r78gx3kdw15cburq.png)

So, you have started to write a nice app or library in [Solid.js](https://www.solidjs.com/) and TypeScript - good choice, by the way - but now you want to unit test everything to make sure nothing breaks.

![Jest logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fnnfif593kiswfmwwen.jpg)

[Jest](https://jestjs.io/) is currently one of the best options for front end testing, but it requires some setup to play nicely with Solid.js. There are two options:

* `solid-jest` - a preset to setup jest transpile solid using babel, given a working babel configuration; won't type-check, but is faster
* `ts-jest` - a module to use TypeScript with jest that needs to be coerced a bit to work with Solid.js

If you are using TypeScript, choosing `solid-jest` might save  you time when running the tests at the expense of missing type checks, but those can be run separately. `ts-jest` will check the types for you, but will take a bit longer. Both choices are valid, so decide for yourself.

## Configuration

Regardless if you use solid-jest or ts-jest, you will need a babel config that supports Solid.js - the main difference is where to put it. It looks like this:

```js
{
  "presets": [
    "@babel/preset-env",
    "babel-preset-solid",
    // only if you use TS with solid-jest
    "@babel/preset-typescript"
  ]
}
```

To be able to run it, you need to add the dev dependencies `@babel/core`, `@babel/preset-env` and optionally `@babel/preset-typescript` depending on if you use TypeScript or not.

If you decided to use `solid-jest`, then put the babel config in your `.babelrc` file at the project root; otherwise it goes into the jest config section e.g. in package.json/jest as shown in the upcoming `ts-jest` section.

### solid-jest

For `solid-jest`, the jest config that requires `.babelrc` to run looks like this (without the comment):

```js
{
  "jest" : {
    "preset": "solid-jest/preset/browser",
    // insert setupFiles and other config
  }
}
```

### ts-jest

If you use `ts-jest` instead, it looks like this (without the comments):

```js
{ 
  "jest": {
    "preset": "ts-jest",
    "globals": {
      "ts-jest": {
        "tsconfig": "tsconfig.json",
        "babelConfig": {
          "presets": [
            "babel-preset-solid",
            "@babel/preset-env"
          ]
        }
      }
    },
    // insert setupFiles and other config
    // you probably want to test in browser mode:
    "testEnvironment": "jsdom",
    // unfortunately, solid cannot detect browser mode here,
    // so we need to manually point it to the right versions:
    "moduleNameMapper": {
      "solid-js/web": "<rootDir>/node_modules/solid-js/web/dist/web.cjs",
      "solid-js": "<rootDir>/node_modules/solid-js/dist/solid.cjs"
    }
  }
}
```

## What now?

I assume you already know your way around jest. If not, let me point you at this [article series about jest](https://dev.to/dstrekelj/how-to-write-unit-tests-in-javascript-with-jest-2e83). So why is here more text, you ask? There are a few more considerations when testing Solid.js code:

- effects and memos only work inside a reactive root
- components output HTMLElements or functions that returns them (either a single one or an array).

That means you can use a few shortcuts instead of actual rendering everything and look at it from a DOM perspective. Let's look at the available shortcuts:

### Testing custom primitives ("hooks")

A powerful way to make functionality reusable is to put it into a separate primitive function. Let's have a naive implementation of a function that returns a variable number of words of "Lorem ipsum" text:

```ts
const loremIpsumWords = 'Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split(/\s+/);

const createLorem = (words: Accessor<number> | number) => {
  return createMemo(() => {
    const output = [],
      len = typeof words === 'function' ? words() : words;
    while (output.length <= len) {
      output.push(...loremIpsumWords);
    }

    return output.slice(0, len).join(' ');
  });
};
```

If we use `words` as an Accessor, it will only ever be updated inside a reactive root and the updates only available inside an effect. Luckily, jest will happily wait for async functions, so we can use a promise to collect the output that will finally be evaluated.

```ts
test(
  'it updates the result when words update',
  async () => {
    const input = [3, 2, 5],
      expectedOutput = [
        'Lorem ipsum dolor',
        'Lorem ipsum',
        'Lorem ipsum dolor sit amet'
      ];
  
    const actualOutput = await new Promise<string[]>(resolve => createRoot(dispose => {
      const [words, setWords] = createSignal(input.shift() ?? 3);
      const lorem = createLorem(words);

      const output: string[] = [];
      createEffect(() => {
        // effects are batched, so the escape condition needs
        // to run after the output is complete:
        if (input.length === 0) {
          dispose();
          resolve(output);
        }
        output.push(lorem());
        setWords(input.shift() ?? 0);
      });
    }));

    expect(actualOutput).toEqual(expectedOutput);
  }
);
```

This way, we don't even need to render the output and are very flexible with the test cases.

### Testing directives (`use:...`)

A [custom directive](https://www.solidjs.com/docs/latest/api#use%3A___) is merely a primitive that receives a DOM reference and an Accessor with arguments, if there are any. Let's consider a directive that abstracts the Fullscreen API. It would have the following signature:

```ts
export type FullscreenDirective = (
  ref: HTMLElement,
  active: Accessor<boolean | FullscreenOptions>
) => void;
```

and is used like this:

```tsx
const [fs, setFs] = createSignal(false);
return <div use:FullscreenDirective={fs}>...</div>;
```

Do we need to render now to test the directive? No, we don't! We can just create our HTMLElement by using `document.createElement('div')` and test it like our previous primitive.

While you could argue that you want to test things from the user's perspective, the user of a directive is still a developer that uses it in the component - and you don't need to test if Solid.js actually works, that's already done for you by the maintainers.

You can have a look at an actual example at the [fullscreen primitive from `solid-primitives`](https://github.com/davedbase/solid-primitives/blob/main/packages/fullscreen/test/index.test.ts#L22) written by yours truly.

### Testing components

Finally, we get to use render!? No? Don't tell me you're testing the returned DOM elements without ever rendering them!?

Well, you certainly could do that, but not even I'm suggesting you should, because components are usually used in a DOM context and not solely in a component context, as rendering could introduce side effects. Instead, I want to point you at [Solid's testing library](https://github.com/solidjs/solid-testing-library).

So let's add `solid-testing-library` to our project, together with `@testing-library/jest-dom` and configure it:

```js
// in package.json
{
  // ...
  "jest": {
    "preset": "solid-jest/preset/browser",
    "setupFilesAfterEnv": ["./src/setupTests.ts"]
  }
}

// and in ./src/setupTests.ts:
import "regenerator-runtime/runtime";
import '@testing-library/jest-dom'
```

Our project contains the following nonsensical component that we want be tested:

```tsx
import { createSignal, Component, JSX } from 'solid-js';

export const MyComponent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
  const [clicked, setClicked] = createSignal(false);
  return <div {...props} role="button" onClick={() => setClicked(true)}>
    {clicked() ? 'Test this!' : 'Click me!'}
  </div>;
};
```

Now let's write a simple test using the testing library:

```tsx
import { screen, render, fireEvent } from 'solid-testing-library';
import { MyComponent } from './my-component';

test('changes text on click', async () => {
  await render(() => <MyComponent />);
  const component = await screen.findByRole('button', { name: 'Click me!' });
  expect(component).toBeInTheDocument();
  fireEvent.click(component);
  expect(await screen.findByRole('button', { name: 'Test this!' })).toBeInTheDocument();
});
```

Let's run this:

```sh
> jest

 PASS  src/testing.test.tsx
  ✓ changes text on click (53 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.012 s
Ran all test suites.
```

May your tests catch all the bugs!