npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fictjs/testing-library

v0.16.0

Published

Testing utilities for Fict components, built on @testing-library/dom

Downloads

1,341

Readme

@fictjs/testing-library

Testing utilities for Fict components, built on top of @testing-library/dom.

Table of Contents

Overview

@fictjs/testing-library provides a set of utilities for testing Fict components in a manner similar to @testing-library/react and @solidjs/testing-library. It integrates seamlessly with Fict's reactive runtime while leveraging the familiar query APIs from @testing-library/dom.

Key Features

| Feature | Description | | ---------------------- | ---------------------------------------------------- | | render() | Render Fict components and get query utilities | | cleanup() | Automatic cleanup of rendered components | | renderHook() | Test custom reactive hooks in isolation | | testEffect() | Test effects with async assertions | | act() | Flush pending microtasks and effects | | Error Boundary Testing | Test error handling with renderWithErrorBoundary() | | Suspense Testing | Test loading states with renderWithSuspense() |

Installation

pnpm add -D @fictjs/testing-library
# or
npm install --save-dev @fictjs/testing-library
# or
yarn add -D @fictjs/testing-library

Peer Dependencies:

pnpm add @fictjs/runtime @testing-library/dom

Quick Start

Basic Component Testing

import { render, screen, cleanup } from '@fictjs/testing-library'
import { describe, it, expect, afterEach } from 'vitest'

// Cleanup is automatic with Vitest/Jest
describe('Greeting', () => {
  it('renders greeting message', () => {
    const { getByText } = render(() => <Greeting name="World" />)

    expect(getByText('Hello, World!')).toBeInTheDocument()
  })
})

Testing with User Interactions

import { render, fireEvent } from '@fictjs/testing-library'

it('increments counter on click', async () => {
  const { getByRole, getByText } = render(() => <Counter />)

  const button = getByRole('button')
  fireEvent.click(button)

  expect(getByText('Count: 1')).toBeInTheDocument()
})

Testing Reactive Hooks

import { renderHook } from '@fictjs/testing-library'

function useCounter(initial: number) {
  let count = $state(initial)
  const increment = () => count++
  return { count: () => count, increment }
}

it('increments counter', () => {
  const { result } = renderHook(() => useCounter(0))

  expect(result.current.count()).toBe(0)
  result.current.increment()
  expect(result.current.count()).toBe(1)
})

Core Concepts

1. Component Rendering

The render() function renders a Fict component into a container and returns query utilities bound to that container:

const { container, getByText, queryByRole } = render(() => <MyComponent />)

The component is rendered inside a reactive root, enabling full reactivity during tests.

2. Automatic Cleanup

By default, cleanup runs automatically after each test when using Vitest or Jest. This can be disabled via the FICT_TL_SKIP_AUTO_CLEANUP environment variable.

// Cleanup runs automatically - no manual cleanup needed!
it('first test', () => {
  render(() => <ComponentA />)
})

it('second test', () => {
  render(() => <ComponentB />) // Previous render is already cleaned up
})

3. Testing Reactive Code

Use renderHook() to test reactive hooks and custom logic:

const { result, rerender, cleanup } = renderHook(initial => useMyHook(initial), {
  initialProps: [10],
})

// Access hook return values
result.current.someMethod()

// Rerender with new props (note: state resets on rerender)
rerender([20])

Note: rerender() disposes the previous root and creates a new one. Hook state does not persist across rerenders. Disposal runs effect cleanups from the previous root before mounting the new hook instance.

4. Testing Effects

Use testEffect() for testing async effects:

const result = await testEffect<string>(done => {
  const data = $state<string | null>(null)

  $effect(() => {
    if (data !== null) {
      done(data) // Signal completion
    }
  })

  // Simulate async operation
  setTimeout(() => {
    data = 'loaded'
  }, 100)
})

expect(result).toBe('loaded')

5. Flushing Updates

Use act() to ensure all pending microtasks and effects are flushed:

await act(() => {
  result.current.increment()
})

// All effects have run, assertions are safe
expect(result.current.count()).toBe(1)

API Reference

render

Render a Fict component for testing.

function render<Q extends Queries = typeof queries>(
  view: View,
  options?: RenderOptions<Q>,
): RenderResult<Q>

Parameters:

| Name | Type | Description | | --------------------- | ---------------- | ----------------------------------------------------------------- | | view | () => FictNode | A function that returns the component to render | | options.container | HTMLElement? | Container element to render into | | options.baseElement | HTMLElement? | Base element for queries (defaults to container or document.body) | | options.queries | Queries? | Custom queries to use | | options.wrapper | Component? | Wrapper component (e.g., for context providers) |

Returns:

| Property | Type | Description | | ------------------------------- | ---------------------- | --------------------------------------- | | container | HTMLElement | The container element | | baseElement | HTMLElement | The base element for queries | | asFragment() | () => string | Returns the container's innerHTML | | debug() | DebugFn | Pretty-print the DOM to console | | unmount() | () => void | Unmount and cleanup | | rerender(view) | (view: View) => void | Re-render with a new view | | getByText, queryByRole, ... | Query functions | All queries from @testing-library/dom |

Example:

// Basic usage
const { getByText } = render(() => <MyComponent />)
expect(getByText('Hello')).toBeInTheDocument()

// With wrapper for context
const { getByText } = render(() => <MyComponent />, {
  wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
})

// With custom container
const container = document.createElement('div')
render(() => <MyComponent />, { container })

cleanup

Clean up all rendered components.

function cleanup(): void

Called automatically after each test. Can be called manually if needed.


renderHook

Render a hook/reactive code for testing.

function renderHook<Result, Props extends unknown[] = []>(
  hookFn: (...args: Props) => Result,
  options?: RenderHookOptions<Props> | Props,
): RenderHookResult<Result, Props>

Parameters:

| Name | Type | Description | | ---------------------- | ---------------------------- | --------------------------------- | | hookFn | (...args: Props) => Result | The hook function to test | | options.initialProps | Props? | Initial props to pass to the hook | | options.wrapper | Component? | Wrapper component |

Returns:

| Property | Type | Description | | ------------------ | ------------------------- | ------------------------------------------------ | | result | { current: Result } | Container holding the hook's return value | | rerender(props?) | (props?: Props) => void | Remount with new props (previous state disposed) | | cleanup() | () => void | Clean up the hook | | unmount() | () => void | Alias for cleanup |

Example:

// Test a counter hook
const { result } = renderHook((initial: number) => useCounter(initial), {
  initialProps: [10],
})

expect(result.current.count()).toBe(10)
result.current.increment()
expect(result.current.count()).toBe(11)

// Shorthand array syntax for initial props
const { result } = renderHook((a, b) => useMyHook(a, b), ['foo', 42])

testEffect

Test an effect asynchronously.

function testEffect<T = void>(fn: TestEffectCallback<T>): Promise<T>

type TestEffectCallback<T> = (done: (result: T) => void) => void

Example:

// Test async data loading
const result = await testEffect<number>(done => {
  const count = $state(0)

  $effect(() => {
    if (count === 3) {
      done(count)
    }
  })

  count = 1
  count = 2
  count = 3 // Triggers done()
})

expect(result).toBe(3)

act

Run updates and flush pending microtasks/effects.

async function act<T>(fn: () => T | Promise<T>): Promise<T>

Example:

await act(() => {
  result.current.increment()
})
// All effects have been flushed

flush

Flush pending microtasks.

function flush(): Promise<void>

waitForCondition

Wait for a condition to be true.

function waitForCondition(
  condition: () => boolean,
  options?: { timeout?: number; interval?: number },
): Promise<void>

Example:

await waitForCondition(() => element.textContent === 'Loaded', {
  timeout: 1000,
  interval: 50,
})

renderWithErrorBoundary

Render a view wrapped in an ErrorBoundary.

function renderWithErrorBoundary<Q extends Queries>(
  view: View,
  options?: ErrorBoundaryRenderOptions<Q>,
): ErrorBoundaryRenderResult<Q>

Additional Options:

| Name | Type | Description | | ----------- | ---------------------------------------- | ----------------------------- | | fallback | FictNode \| ((err, reset) => FictNode) | Fallback UI when error occurs | | onError | (err: unknown) => void | Callback when error is caught | | resetKeys | unknown \| (() => unknown) | Keys that trigger a reset |

Additional Returns:

| Property | Type | Description | | ---------------------- | ------------------------ | -------------------------- | | triggerError(error) | (error: Error) => void | Trigger an error | | resetErrorBoundary() | () => void | Reset the error boundary | | isShowingFallback() | () => boolean | Check if fallback is shown |

Example:

const onError = vi.fn()
const { isShowingFallback, triggerError } = renderWithErrorBoundary(
  () => <ComponentThatMightThrow />,
  {
    fallback: err => <div data-testid="error">{err.message}</div>,
    onError,
  },
)

triggerError(new Error('Test error'))
expect(isShowingFallback()).toBe(true)
expect(onError).toHaveBeenCalled()

renderWithSuspense

Render a view wrapped in a Suspense boundary.

function renderWithSuspense<Q extends Queries>(
  view: View,
  options?: SuspenseRenderOptions<Q>,
): SuspenseRenderResult<Q>

Additional Options:

| Name | Type | Description | | ----------- | ------------------------------ | ------------------------------- | | fallback | FictNode \| (() => FictNode) | Loading UI while suspended | | onResolve | () => void | Callback when suspense resolves | | onReject | (err: unknown) => void | Callback when suspense rejects |

Additional Returns:

| Property | Type | Description | | ----------------------------- | ----------------------------------------- | ---------------------------------- | | isShowingFallback() | () => boolean | Check if loading fallback is shown | | waitForResolution(options?) | (options?: {timeout?}) => Promise<void> | Wait for suspense to resolve |

Example:

const { token, resolve } = createTestSuspenseToken()

const { isShowingFallback, waitForResolution } = renderWithSuspense(
  () => <AsyncComponent token={token} />,
  { fallback: <div>Loading...</div> },
)

expect(isShowingFallback()).toBe(true)

resolve()
await waitForResolution()

expect(isShowingFallback()).toBe(false)

createTestSuspenseToken

Create a test suspense token for controlling suspense in tests.

function createTestSuspenseToken(): TestSuspenseHandle

interface TestSuspenseHandle {
  token: { then: PromiseLike<void>['then'] }
  resolve: () => void
  reject: (err: unknown) => void
}

Example:

const { token, resolve, reject } = createTestSuspenseToken()

// Throw token in component to trigger suspense
const Component = () => {
  throw token
}

// Later, resolve to continue rendering
resolve()

Type Definitions

Core Types

/** A Fict view function */
type View = () => FictNode

/** Render options */
interface RenderOptions<Q extends Queries> {
  container?: HTMLElement
  baseElement?: HTMLElement
  queries?: Q
  wrapper?: Component<{ children: FictNode }>
}

/** RenderHook options */
interface RenderHookOptions<Props extends unknown[]> {
  initialProps?: Props
  wrapper?: Component<{ children: FictNode }>
}

/** TestEffect callback */
type TestEffectCallback<T> = (done: (result: T) => void) => void

Result Types

/** Render result */
type RenderResult<Q extends Queries> = BoundFunctions<Q> & {
  asFragment: () => string
  container: HTMLElement
  baseElement: HTMLElement
  debug: DebugFn
  unmount: () => void
  rerender: (newView: View) => void
}

/** RenderHook result */
interface RenderHookResult<Result, Props extends unknown[]> {
  result: { current: Result }
  rerender: (newProps?: Props) => void
  cleanup: () => void
  unmount: () => void
}

Testing Patterns

Testing with Context Providers

const AllProviders = ({ children }: { children: FictNode }) => (
  <ThemeProvider>
    <AuthProvider>{children}</AuthProvider>
  </ThemeProvider>
)

const { getByText } = render(() => <MyComponent />, {
  wrapper: AllProviders,
})

Testing Reactive State Changes

import { renderHook, act } from '@fictjs/testing-library'

it('handles state updates', async () => {
  const { result } = renderHook(() => {
    const count = createSignal(0)
    return { count, inc: () => count(count() + 1) }
  })

  await act(() => {
    result.current.inc()
  })

  expect(result.current.count()).toBe(1)
})

Testing Async Effects

import { testEffect } from '@fictjs/testing-library'

it('loads data asynchronously', async () => {
  const result = await testEffect<Data>(done => {
    const data = $state<Data | null>(null)

    $effect(() => {
      if (data) done(data)
    })

    fetchData().then(d => {
      data = d
    })
  })

  expect(result).toEqual(expectedData)
})

Testing Error Boundaries

it('shows error fallback when child throws', () => {
  const { isShowingFallback, triggerError, getByTestId } = renderWithErrorBoundary(
    () => <ChildComponent />,
    {
      fallback: err => <div data-testid="error">{String(err)}</div>,
    },
  )

  triggerError(new Error('Something went wrong'))

  expect(isShowingFallback()).toBe(true)
  expect(getByTestId('error').textContent).toContain('Something went wrong')
})

Troubleshooting

Common Issues

1. "Cleanup not running between tests"

Cause: Auto-cleanup may not be set up correctly.

Solution: Ensure you're using Vitest or Jest, or manually call cleanup():

import { cleanup } from '@fictjs/testing-library'

afterEach(() => {
  cleanup()
})

2. "Hook state resets on rerender"

Cause: This is expected behavior. renderHook().rerender() disposes the previous root and creates a new one.

Solution: If you need persistent state, test it within a single hook execution:

const { result } = renderHook(() => useMyHook())
result.current.increment() // State persists here
result.current.increment() // And here
// rerender() creates a fresh hook instance

3. "Effects not running in tests"

Cause: Effects are scheduled asynchronously.

Solution: Use act() or await with flush():

await act(() => {
  result.current.triggerEffect()
})
// Effects have now run

4. "Queries not finding elements"

Cause: Element may not be in the queried container.

Solution: Use debug() to inspect the DOM:

const { debug, queryByText } = render(() => <MyComponent />)
debug() // Prints the DOM to console

Configuration

Disabling Auto-Cleanup

Set the environment variable:

FICT_TL_SKIP_AUTO_CLEANUP=1 npm test

Using Custom Queries

import { buildQueries } from '@testing-library/dom'

const queryAllByDataCy = (container, value) =>
  container.querySelectorAll(`[data-cy="${value}"]`)

const [
  queryByDataCy,
  getAllByDataCy,
  getByDataCy,
  findAllByDataCy,
  findByDataCy,
] = buildQueries(queryAllByDataCy, ...)

const { getByDataCy } = render(() => <MyComponent />, {
  queries: { getByDataCy, queryByDataCy, ... }
})

Related Packages

  • @fictjs/runtime - Core reactive runtime
  • @fictjs/compiler - JSX compiler
  • @fictjs/vite-plugin - Vite integration
  • @testing-library/dom - DOM testing utilities

License

MIT