|
| 1 | +--- |
| 2 | +id: api |
| 3 | +title: API |
| 4 | +sidebar_label: API |
| 5 | +--- |
| 6 | + |
| 7 | +Due to being inspired by the [preact-testing-library][preact-docs] you can check |
| 8 | +its page for more information. |
| 9 | + |
| 10 | +[preact-docs]: ../preact-testing-library/intro.mdx |
| 11 | + |
| 12 | +There are several key differences, to be aware of. |
| 13 | + |
| 14 | +- [`render`](#render) |
| 15 | +- [`renderHook`](#renderHook) |
| 16 | +- [`renderDirective`](#renderDirective) |
| 17 | +- [`Async methods`](#async-methods) |
| 18 | +- [`Know issues`](#known-issues) |
| 19 | + |
| 20 | +## `render` |
| 21 | + |
| 22 | +The `render` function takes in a function that returns a Solid Component, rather |
| 23 | +than simply the component itself. |
| 24 | + |
| 25 | +```tsx |
| 26 | +const results = render(() => <YourComponent />, options) |
| 27 | +``` |
| 28 | + |
| 29 | +Solid.js does _not_ re-render, it merely executes side effects triggered by |
| 30 | +reactive state that change the DOM, therefore there is no `rerender` method. You |
| 31 | +can use global signals to manipulate your test component in a way that causes it |
| 32 | +to update. |
| 33 | + |
| 34 | +In addition to the original API, the render function of this testing library |
| 35 | +supports a convenient `location` option that will set up an in-memory router |
| 36 | +pointing at the specified location. Since this setup is not synchronous, you |
| 37 | +need to first use asynchronous queries (`findBy`) after employing it: |
| 38 | + |
| 39 | +```tsx |
| 40 | +it('uses params', async () => { |
| 41 | + const App = () => ( |
| 42 | + <> |
| 43 | + <Route path="/ids/:id" component={() => <p>Id: {useParams()?.id}</p>} /> |
| 44 | + <Route path="/" component={() => <p>Start</p>} /> |
| 45 | + </> |
| 46 | + ) |
| 47 | + const {findByText} = render(() => <App />, {location: 'ids/1234'}) |
| 48 | + expect(await findByText('Id: 1234')).not.toBeFalsy() |
| 49 | +}) |
| 50 | +``` |
| 51 | + |
| 52 | +It uses `@solidjs/router`, so if you want to use a different router, you should |
| 53 | +consider the `wrapper` option instead. If you attempt to use this without having |
| 54 | +the package installed, you will receive an error message. |
| 55 | + |
| 56 | +## `renderHook` |
| 57 | + |
| 58 | +Solid.js external reactive state does not require any DOM elements to run in, so |
| 59 | +our `renderHook` call to test hooks in the context of a component (if your hook |
| 60 | +does not require the context of a component, `createRoot` should suffice to test |
| 61 | +the reactive behavior; for convenience, we also have `createEffect`, which is |
| 62 | +described in the [`Async methods`](#async-methods) section) has no `container`, |
| 63 | +`baseElement` or queries in its options or return value. Instead, it has an |
| 64 | +`owner` to be used with |
| 65 | +[`runWithOwner`](https://www.solidjs.com/docs/latest/api#runwithowner) if |
| 66 | +required. It also exposes a `cleanup` function, though this is already |
| 67 | +automatically called after the test is finished. |
| 68 | + |
| 69 | +```ts |
| 70 | +function renderHook<Args extends any[], Result>( |
| 71 | + hook: (...args: Args) => Result, |
| 72 | + options: { |
| 73 | + initialProps?: Args, |
| 74 | + wrapper?: Component<{ children: JSX.Element }> |
| 75 | + } |
| 76 | +) => { |
| 77 | + result: Result; |
| 78 | + owner: Owner | null; |
| 79 | + cleanup: () => void; |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +This can be used to easily test a hook / primitive: |
| 84 | + |
| 85 | +```ts |
| 86 | +const {result} = renderHook(createResult) |
| 87 | +expect(result).toBe(true) |
| 88 | +``` |
| 89 | + |
| 90 | +If you are using a `wrapper` with `renderHook`, make sure it will **always** |
| 91 | +return `props.children` - especially if you are using a context with |
| 92 | +asynchronous code together with `<Show>`, because this is required to get the |
| 93 | +value from the hook and it is only obtained synchronously once and you will |
| 94 | +otherwise only get `undefined` and wonder why this is the case. |
| 95 | + |
| 96 | +## `renderDirective` |
| 97 | + |
| 98 | +Solid.js supports |
| 99 | +[custom directives](https://www.solidjs.com/docs/latest/api#use___), which is a |
| 100 | +convenient pattern to tie custom behavior to elements, so we also have a |
| 101 | +`renderDirective` call, which augments `renderHook` to take a directive as first |
| 102 | +argument, accept an `initialValue` for the argument and a `targetElement` |
| 103 | +(string, HTMLElement or function returning an HTMLElement) in the `options` and |
| 104 | +also returns `arg` and `setArg` to read and manipulate the argument of the |
| 105 | +directive. |
| 106 | + |
| 107 | +```ts |
| 108 | +function renderDirective< |
| 109 | + Arg extends any, |
| 110 | + Elem extends HTMLElement |
| 111 | +>( |
| 112 | + directive: (ref: Elem, arg: Accessor<Arg>) => void, |
| 113 | + options?: { |
| 114 | + ...renderOptions, |
| 115 | + initialValue: Arg, |
| 116 | + targetElement: |
| 117 | + | Lowercase<Elem['nodeName']> |
| 118 | + | Elem |
| 119 | + | (() => Elem) |
| 120 | + } |
| 121 | +): Result & { arg: Accessor<Arg>, setArg: Setter<Arg> }; |
| 122 | +``` |
| 123 | + |
| 124 | +This allows for very effective and concise testing of directives: |
| 125 | + |
| 126 | +```ts |
| 127 | +const {asFragment, setArg} = renderDirective(myDirective) |
| 128 | +expect(asFragment()).toBe('<div data-directive="works"></div>') |
| 129 | +setArg('perfect') |
| 130 | +expect(asFragment()).toBe('<div data-directive="perfect"></div>') |
| 131 | +``` |
| 132 | + |
| 133 | +## Async methods |
| 134 | + |
| 135 | +Solid.js reactive changes are pretty instantaneous, so there is rarely need to |
| 136 | +use `waitFor(…)`, `await findByRole(…)` and other asynchronous queries to test |
| 137 | +the rendered result, except for transitions, suspense, resources and router |
| 138 | +navigation. |
| 139 | + |
| 140 | +Solid.js manages side effects with different variants of `createEffect`. While |
| 141 | +you can use `waitFor` to test asynchronous effects, it uses polling instead of |
| 142 | +allowing Solid's reactivity to trigger the next step. In order to simplify |
| 143 | +testing those asynchronous effects, we have a `testEffect` helper that |
| 144 | +complements the hooks for directives and hooks: |
| 145 | + |
| 146 | +```ts |
| 147 | +testEffect(fn: (done: (result: T) => void) => void, owner?: Owner): Promise<T> |
| 148 | + |
| 149 | +// use it like this: |
| 150 | +test("testEffect allows testing an effect asynchronously", () => { |
| 151 | + const [value, setValue] = createSignal(0); |
| 152 | + return testEffect(done => createEffect((run: number = 0) => { |
| 153 | + if (run === 0) { |
| 154 | + expect(value()).toBe(0); |
| 155 | + setValue(1); |
| 156 | + } else if (run === 1) { |
| 157 | + expect(value()).toBe(1); |
| 158 | + done(); |
| 159 | + } |
| 160 | + return run + 1; |
| 161 | + })); |
| 162 | +}); |
| 163 | +``` |
| 164 | + |
| 165 | +It allows running the effect inside a defined owner that is received as an |
| 166 | +optional second argument. This can be useful in combination with `renderHook`, |
| 167 | +which gives you an owner field in its result. The return value is a Promise with |
| 168 | +the value given to the `done()` callback. You can either await the result for |
| 169 | +further assertions or return it to your test runner. |
| 170 | + |
| 171 | +## Known issues |
| 172 | + |
| 173 | +If you are using [`vitest`](https://vitest.dev/), then tests might fail, because |
| 174 | +the packages `solid-js`, and `@solidjs/router` (if used) need to be loaded only |
| 175 | +once, and they could be loaded both through the internal `vite` server and |
| 176 | +through node. Typical bugs that happen because of this is that dispose is |
| 177 | +supposedly undefined, or the router could not be loaded. |
| 178 | + |
| 179 | +Since version 2.8.2, our vite plugin has gained the capability to configure |
| 180 | +everything for testing, so you should only need extra configuration for globals, |
| 181 | +coverage, etc. |
0 commit comments