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

@glyx-dev/testing Stable

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

What it does:

  • Mocks all __glyx_* native bindings so React components that call Glyx 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

npm install --save-dev @glyx-dev/testing

Setup

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

# bunfig.toml
[test]
preload = ["@glyx-dev/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 __glyx_* native binding on globalThis. Must be called before importing any Glyx component under test.

import { installStubs } from '@glyx-dev/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-reconciler with an in-memory host so event handlers and state are real. Falls back to react-dom/server if the reconciler is unavailable.

render(element: ReactElement): Promise<{
  container:      object
  getByText:      (text: string) => Props
  queryByText:    (text: string) => Props | null
  getAllByText:    (text: string) => Props[]
  getByTestId:    (testId: string) => Props
  queryByTestId:  (testId: string) => Props | null
  getAllByTestId:  (testId: string) => Props[]
  debug:          () => void
  unmount:        () => void
}>
import { render } from '@glyx-dev/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): Props          // throws if not found
screen.queryByText(text: string): Props | null // returns null if not found
screen.getAllByText(text: string): Props[]      // throws if none found
screen.getByTestId(id: string): Props          // throws if not found
screen.queryByTestId(id: string): Props | null // returns null if not found
screen.getAllByTestId(id: string): Props[]      // throws if none found
screen.debug(): void                           // prints node tree
import { render, screen } from '@glyx-dev/testing'
 
test('shows error message', async () => {
  await render(<LoginForm />)
  screen.debug()
  expect(screen.queryByText('Invalid password')).toBeNull()
})

getByTestId / queryByTestId / getAllByTestId

Query nodes by the testID prop. Prefer testID queries over text queries when the text might change or isn't exposed to users (e.g. icon buttons, containers).

// In your component:
function UserCard({ user }) {
  return (
    <View testID="user-card">
      <Text testID="user-name">{user.name}</Text>
      <Pressable testID="follow-btn" onPress={handleFollow}>
        <Text>Follow</Text>
      </Pressable>
    </View>
  )
}
 
// In your test:
const { getByTestId } = await render(<UserCard user={{ name: 'Alice' }} />)
 
expect(getByTestId('user-card')).toBeTruthy()
expect(getByTestId('user-name').children).toBe('Alice')
 
fireEvent.press(getByTestId('follow-btn'))

The testID prop is also tracked on the Rust side so future integration tests can locate native nodes by ID.


getNodeTree()

Return a Map snapshot of the full in-memory node tree after a render() call. Useful for snapshot tests or low-level structure assertions.

getNodeTree(): Map<number, { id: number; type: string; props: object; children: number[] }>
import { render, getNodeTree } from '@glyx-dev/testing'
 
test('tree structure', async () => {
  await render(<UserCard user={{ name: 'Alice' }} />)
  const tree = getNodeTree()
 
  const named = [...tree.values()].filter((n) => n.props.testID === 'user-card')
  expect(named).toHaveLength(1)
})

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 '@glyx-dev/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 '@glyx-dev/testing'
import { test, expect, mock } from 'bun:test'
 
test('TextInput fires onChange', async () => {
  const onChange = mock(() => {})
  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 '@glyx-dev/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 __glyx_* binding, overriding the auto-stub.

mockBinding(name: string, impl: Function): void
import { installStubs, mockBinding, render } from '@glyx-dev/testing'
 
beforeAll(() => {
  installStubs()
 
  // Override the fetch stub to return real fixture data
  mockBinding('__glyx_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, act, waitFor } from '@glyx-dev/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 Glyx APIs

Components that call Glyx 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 '@glyx-dev/testing'
 
beforeAll(() => {
  installStubs()
 
  const store = new Map()
  mockBinding('__glyx_credentials_set', (key, val) => {
    store.set(key, val)
    return Promise.resolve(null)
  })
  mockBinding('__glyx_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())
})

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