Solid Testing Library

repository·main·Indexed 42 days ago

https://github.com/solidjs/solid-testing-library

A lightweight testing utility for SolidJS components, version 0.8.9, designed to encourage testing practices that resemble actual software usage. It provides specialized functions including `render` for components, `renderHook` for hooks and primitives, `renderDirective` for custom directives, and `testEffect` for asynchronous effects. It integrates with @solidjs/router for in-memory routing and supports standard @testing-library/dom queries.

Tokens
4K
Snippets
15
Records
15
Agent score
53%

What's inside Solid Testing Library

  1. Install @solidjs/testing-library

    main

    Install the library as a development dependency using npm:

    npm install --save-dev @solidjs/testing-library

    Recommendations

    • Jest Users: Use solid-jest to ensure the browser version of Solid is resolved instead of the Node/server version.
    • Matchers: If using Jest or Vitest, consider installing @testing-library/jest-dom to enable custom DOM matchers.
  2. Test asynchronous effects with testEffect

    main

    Testing asynchronous createEffect calls using waitFor relies on polling, which doesn't leverage Solid's reactivity. To test effects more naturally, use testEffect.

    testEffect returns a Promise that resolves when the done() callback is invoked. You can await this promise or return it to your test runner.

    Parameters:

    • fn: A function that receives a done callback. Inside this function, you define your effect logic.
    • owner (optional): An optional Owner to run the effect inside. You can pass the owner returned by renderHook here.
    testEffect(fn: (done: (result: T) => void) => void, owner?: Owner): Promise<T>
    
    // Example usage
    test("testEffect allows testing an effect asynchronously", () => {
      const [value, setValue] = createSignal(0);
      return testEffect(done => createEffect((run: number = 0) => {
        if (run === 0) {
          expect(value()).toBe(0);
          setValue(1);
        } else if (run === 1) {
          expect(value()).toBe(1);
          done();
        }
        return run + 1;
      }));
    });
  3. Use the render function

    main

    The render function in solid-testing-library differs from other Testing Library implementations. It requires a function that returns a Solid Component, rather than the component element itself.

    Note on Re-rendering: Solid.js does not re-render components in the way React does; it executes side effects triggered by reactive state. Consequently, there is no rerender method. To update a component during a test, use global signals to trigger reactive updates.

    // Correct usage with solid-testing-library
    const results = render(() => <YourComponent />, options);
  4. Test hooks with renderHook

    main

    Use renderHook to test Solid hooks or primitives. Unlike component rendering, renderHook does not provide a container, baseElement, or DOM queries in its return value because reactive state does not require a DOM element to run.

    Return Values:

    • result: The result of the hook.
    • owner: The Owner object, which can be used with runWithOwner if required.
    • cleanup: A function to manually trigger cleanup (though this is called automatically).

    Warning: If using a wrapper with renderHook, ensure the wrapper always returns props.children. If the wrapper uses asynchronous context (like <Show>), failing to return children synchronously may result in the hook receiving undefined.

    // Signature
    function renderHook<Args extends any[], Result>(
      hook: (...args: Args) => Result,
      options: {
        initialProps?: Args,
        wrapper?: Component<{ children: JSX.Element }
      }
    ) => {
      result: Result;
      owner: Owner | null;
      cleanup: () => void;
    }
    
    // Example usage
    const { result } = renderHook(createResult);
    expect(result).toBe(true);
  5. Test custom directives with renderDirective

    main

    The renderDirective function allows for testing Solid custom directives. It augments renderHook to accept a directive and a target element.

    Options:

    • initialValue: The initial value for the directive argument.
    • targetElement: The element the directive is attached to (can be a string name, an HTMLElement, or a function returning an HTMLElement).

    Return Values:

    • arg: An Accessor to read the directive's argument.
    • setArg: A Setter to manipulate the directive's argument.
    // Signature
    function renderDirective<Arg extends any, Elem extends HTMLElement>(
      directive: (ref: Elem, arg: Accessor<Arg>) => void,
      options?: {
        ...renderOptions,
        initialValue: Arg,
        targetElement: Lowercase<Elem['nodeName']> | Elem | (() => Elem)
      }
    ): Result & { arg: Accessor<Arg>, setArg: Setter<Arg> };
    
    // Example usage
    const { asFragment, setArg } = renderDirective(myDirective);
    expect(asFragment()).toBe('<div data-directive="works"></div>');
    setArg("perfect");
    expect(asFragment()).toBe('<div data-directive="perfect"></div>');
  6. Configure in-memory routing with the location option

    main

    The render function supports a location option that sets up an in-memory router using @solidjs/router.

    Important: Because setting up the location is not instantaneous, you must use asynchronous queries (like findBy...) to assert the rendered state.

    Limitations:

    • If you use a router other than @solidjs/router, use the wrapper option instead.
    • useNavigate may not work inside tests because it cannot access the Route context. However, <A href="..." noScroll> inside a <Route> works for switching routes during tests.
    import { render, findByText } from '@solidjs/testing-library';
    import { Route } from '@solidjs/router';
    
    it('uses params', async () => {
      const App = () => (
        <>
          <Route path="/ids/:id" component={() => <p>Id: {useParams()?.id}</p>} />
          <Route path="/" component={() => <p>Start</p>} />
        </>
      );
      const { findByText } = render(() => <App />, { location: "ids/1234" });
      expect(await findByText("Id: 1234")).not.toBeFalsy();
    });
  7. Use the Result object from rendering

    main

    The Result type is returned by rendering functions. It provides access to the DOM container and utility methods for testing:

    • container: The HTMLElement containing the rendered output.
    • baseElement: The root element for queries.
    • debug(): A function to print the current DOM state to the console.
    • unmount(): A function to clean up the rendered component.
    • asFragment(): Returns a string representation of the rendered content.
    • queries: All standard @testing-library/dom query functions (e.g., getByText, queryByRole) are bound to this object.
    export type Result = BoundFunctions<typeof queries> & {
      asFragment: () => string;
      container: HTMLElement;
      baseElement: HTMLElement;
      debug: DebugFn;
      unmount: () => void;
    };
  8. Render Solid components with render()

    main

    Use render to mount a Solid component into a DOM container for testing. It provides access to the container, base element, and various testing library queries (like getByText, queryByRole, etc.).

    Options

    • container: The HTML element to render into (defaults to a new <div>).
    • baseElement: The parent of the container (defaults to document.body).
    • queries: Custom queries to include.
    • hydrate: Set to true to test hydration instead of standard rendering.
    • wrapper: A component that wraps the UI (e.g., for providing context). It must return props.children.
    • location: A string representing a path. If provided, the component is wrapped in a @solidjs/router MemoryRouter at that location.

    Result

    • asFragment(): Returns the container's innerHTML as a string.
    • container: The element where the component was rendered.
    • baseElement: The parent of the container.
    • debug(): Logs the current DOM state to the console.
    • unmount(): Manually unmounts the component.
    • [queries]: Standard Testing Library queries (e.g., getByText, findByRole).
    const { getByText } = render(() => <App />, { wrapper: I18nProvider });
    const button = getByText('Accept');
  9. Test Solid hooks with renderHook()

    main

    Use renderHook to test Solid hooks or primitives. It runs the hook within a reactive root and provides access to the returned value and the reactive owner.

    Options

    • initialProps: An array of arguments to pass to the hook.
    • wrapper: A component that wraps the hook execution and must return props.children.

    Result

    • result: The return value of the hook.
    • owner: The reactive Owner in which the hook is running. You can use this with runWithOwner from solid-js to run other reactive code in the same context.
    • cleanup(): Calls the disposal function of the hook/primitive.
    const { result } = renderHook(useI18n, { wrapper: I18nProvider });
    expect(result.t('test')).toBe('works');
  10. Use renderHook with RenderHookOptions and RenderHookResult

    main

    When testing hooks, you can provide RenderHookOptions to define initial props or a wrapper component. The returned RenderHookResult provides access to the hook's value and a cleanup function.

    RenderHookOptions can be passed as an object or as the initial arguments to the hook.

    RenderHookResult contains:

    • result: The current value returned by the hook.
    • owner: The Solid Owner context.
    • cleanup(): A function to unmount the hook.
    export type RenderHookOptions<A extends any[]> = {
      initialProps?: A;
      wrapper?: Component<{ children: JSX.Element }>;
    } | A;
    
    export type RenderHookResult<R> = {
      result: R;
      owner: Owner | null;
      cleanup: () => void;
    };
  11. Handle asynchronous testing with testEffect()

    main

    Use testEffect to wrap asynchronous logic or effects that need to be awaited in your tests. It creates a reactive root and resolves the promise when the done callback is invoked.

    Signature

    testEffect<T>(fn: (done: (result: T) => void) => void, owner?: Owner): Promise<T>

    await testEffect(async (done) => {
      // perform async work
      done(result);
    });
  12. Test Solid directives with renderDirective()

    main

    Use renderDirective to test custom Solid directives. It mounts a target element and applies the directive to it.

    Options

    • initialValue: The initial value passed to the directive.
    • targetElement: The element the directive should be applied to. Can be a string (tag name), an HTMLElement, or a function returning an HTMLElement.
    • container: The HTML element to render into.
    • baseElement: The parent of the container.
    • queries: Custom queries.
    • hydrate: Whether to test hydration.
    • wrapper: A component to wrap the directive.

    Result

    • arg: An accessor for the current value of the directive.
    • setArg: A function to update the value passed to the directive.
    • asFragment(): Returns the HTML fragment as a string.
    • container: The container element.
    • baseElement: The parent element.
    • debug(): Logs the DOM to the console.
    • unmount(): Unmounts the directive.
    • [queries]: Standard Testing Library queries.
    const called = vi.fn()
    const { getByText, baseContainer } = renderDirective(onClickOutside, { initialValue: called });
    expect(called).not.toBeCalled();
    fireEvent.click(baseContainer);
    expect(called).toBeCalled();