🚧 VeloxKit is pre-release software. APIs may change before v1.0. Get started →
Documentation
Packages
@velox/testing

@velox/testing Stable

Unit testing utilities for VeloxKit apps. Works with Bun's built-in test runner (opens in a new tab) (bun test).

What it does:

  • Mocks all __velox_* native bindings so React components that call Velox APIs run in a plain Bun process — no GPU, no winit, no running window required.
  • Renders components to a string via react-dom/server (SSR-style) for headless query.
  • Provides screen, fireEvent, act, and waitFor helpers in a style familiar to @testing-library/react users.

Install

bun add -d @velox/testing

Setup

Add the setup preload to bunfig.toml so stubs are installed before every test file:

# bunfig.toml
[test]
preload = ["@velox/testing/setup"]

The setup script calls installStubs() once globally. You can also call it manually in beforeEach for finer control.


API reference

installStubs()

Installs sensible no-op stubs for every __velox_* native binding on globalThis. Must be called before importing any VeloxKit component under test.

import { installStubs } from '@velox/testing'
 
beforeAll(() => installStubs())

Stubbed bindings include: scene graph, window, file system, database, network, credentials, clipboard, dialog, notifications, audio, canvas, AI, camera, crash, perf, power, system, storage, deep link, HID, mDNS, WebSocket, vector DB, and backend command dispatch.


render(element)

Render a React element and return query helpers. Uses react-dom/server for server-side markup rendering — no DOM or native window required.

render(element: ReactElement): Promise<{
  container:    string
  getByText:    (text: string) => Node
  queryByText:  (text: string) => Node | null
  getAllByText:  (text: string) => Node[]
  debug:        () => void
}>
import { render } from '@velox/testing'
import { describe, test, expect } from 'bun:test'
import Counter from './Counter'
 
test('renders initial count', async () => {
  const { getByText } = await render(<Counter initialValue={0} />)
  expect(getByText('0')).toBeTruthy()
})

screen

Shorthand query object for the most recently rendered element. Mirrors @testing-library/react's screen export.

screen.getByText(text: string): Node          // throws if not found
screen.queryByText(text: string): Node | null // returns null if not found
screen.getAllByText(text: string): Node[]      // throws if none found
screen.debug(): void                          // prints rendered HTML
import { render, screen } from '@velox/testing'
 
test('shows error message', async () => {
  await render(<LoginForm />)
  screen.debug()
  expect(screen.queryByText('Invalid password')).toBeNull()
})

act(callback)

Wraps state updates and async operations so React can flush them before assertions.

act(callback: () => void | Promise<void>): Promise<void>
import { render, act, screen } from '@velox/testing'
 
test('toggles visibility', async () => {
  const { getByText } = await render(<ToggleDemo />)
  const btn = getByText('Show')
 
  await act(() => {
    btn.onPress?.()
  })
 
  expect(screen.getByText('Hidden content')).toBeTruthy()
})

fireEvent

Simulate user interactions on nodes returned by getByText.

fireEvent.press(node)                    // triggers onPress
fireEvent.changeText(node, text)         // triggers onChangeText / onChange
fireEvent.submitEditing(node)            // triggers onSubmitEditing
import { render, fireEvent, screen } from '@velox/testing'
 
test('TextInput fires onChange', async () => {
  const onChange = jest.fn()
  const { getByText } = await render(
    <TextInput placeholder="Name" onChangeText={onChange} />
  )
 
  const input = getByText('Name')
  fireEvent.changeText(input, 'Alice')
  expect(onChange).toHaveBeenCalledWith('Alice')
})

waitFor(assertion, opts?)

Poll an assertion until it passes or timeout expires. Useful for async state updates.

waitFor(
  assertion: () => void,
  opts?: { timeout?: number; interval?: number }
): Promise<void>
import { render, waitFor, screen } from '@velox/testing'
 
test('loads data asynchronously', async () => {
  await render(<DataLoader />)
 
  await waitFor(() => {
    expect(screen.getByText('Loaded!')).toBeTruthy()
  }, { timeout: 2000 })
})

mockBinding(name, impl)

Register a custom mock for a specific __velox_* binding, overriding the auto-stub.

mockBinding(name: string, impl: Function): void
import { installStubs, mockBinding, render } from '@velox/testing'
 
beforeAll(() => {
  installStubs()
 
  // Override the fetch stub to return real fixture data
  mockBinding('__velox_fetch', () =>
    Promise.resolve(JSON.stringify({
      status: 200,
      ok: true,
      body: JSON.stringify({ notes: [{ id: 1, title: 'Test note' }] }),
      headers: { 'content-type': 'application/json' },
    }))
  )
})
 
test('displays fetched notes', async () => {
  const { getByText } = await render(<NotesList />)
  await waitFor(() => expect(getByText('Test note')).toBeTruthy())
})

Full example

// tests/Counter.test.ts
import { describe, test, expect, beforeAll } from 'bun:test'
import { installStubs, render, fireEvent, screen, waitFor } from '@velox/testing'
import Counter from '../src/Counter'
 
beforeAll(() => installStubs())
 
describe('Counter', () => {
  test('renders initial count', async () => {
    const { getByText } = await render(<Counter initialValue={5} />)
    expect(getByText('5')).toBeTruthy()
  })
 
  test('increments on press', async () => {
    const { getByText } = await render(<Counter initialValue={0} />)
 
    const plusBtn = getByText('+')
    await act(() => fireEvent.press(plusBtn))
    await waitFor(() => expect(screen.getByText('1')).toBeTruthy())
  })
 
  test('decrements on press', async () => {
    const { getByText } = await render(<Counter initialValue={3} />)
 
    await act(() => fireEvent.press(getByText('-')))
    await waitFor(() => expect(screen.getByText('2')).toBeTruthy())
  })
 
  test('reset button returns to zero', async () => {
    const { getByText } = await render(<Counter initialValue={10} />)
 
    await act(() => fireEvent.press(getByText('Reset')))
    await waitFor(() => expect(screen.getByText('0')).toBeTruthy())
  })
})

Run with:

bun test

Testing components that use Velox APIs

Components that call Velox APIs (fetch, audio, credentials, etc.) work in tests because installStubs() stubs every binding. Override individual stubs with mockBinding() to control what data flows through your component:

// Test a component that saves to credentials
import { installStubs, mockBinding, render, fireEvent } from '@velox/testing'
 
beforeAll(() => {
  installStubs()
 
  const store = new Map()
  mockBinding('__velox_credentials_set', (key, val) => {
    store.set(key, val)
    return Promise.resolve(null)
  })
  mockBinding('__velox_credentials_get', (key) => {
    return Promise.resolve(store.get(key) ?? 'null')
  })
})
 
test('saves API key', async () => {
  const { getByText } = await render(<ApiKeyForm />)
 
  fireEvent.changeText(getByText('API Key'), 'sk-abc123')
  fireEvent.press(getByText('Save'))
 
  await waitFor(() => expect(screen.getByText('Saved!')).toBeTruthy())
})

@velox/testing is designed for unit and component tests. For end-to-end testing of a fully running Velox window, see the Testing guide.