🚧 Glyx is pre-release software. APIs may change before v1.0. Get started →
Documentation
Guides
Testing

Testing

Glyx ships @glyx-dev/testing — a purpose-built testing library that stubs every __glyx_* native binding so your components run in a plain Bun process without a GPU, winit, or native window. It uses react-reconciler with an in-memory host, so state, effects, and event handlers are all real.

Setup

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

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

# bunfig.toml
[test]
preload = ["@glyx-dev/testing/setup"]

Then run:

bun test

Unit testing utilities

For pure functions and hooks that don't touch native Glyx APIs, no setup is needed:

// src/utils/format.test.ts
import { describe, test, expect } from 'bun:test'
import { formatDate } from './format'
 
describe('formatDate', () => {
  test('formats a timestamp', () => {
    expect(formatDate(1717000000000)).toBe('May 29, 2024')
  })
})

Component testing

Use render, screen, fireEvent, and act from @glyx-dev/testing:

// src/components/NoteCard.test.tsx
import { describe, test, expect, mock } from 'bun:test'
import { render, screen, fireEvent, act } from '@glyx-dev/testing'
import { NoteCard } from './NoteCard'
 
describe('NoteCard', () => {
  test('renders the note title', async () => {
    await render(<NoteCard title="Hello" body="World" />)
    expect(screen.getByText('Hello')).toBeTruthy()
  })
 
  test('calls onPress when tapped', async () => {
    const onPress = mock(() => {})
    const { getByTestId } = await render(
      <NoteCard testID="note-card" title="Hello" onPress={onPress} />
    )
    fireEvent.press(getByTestId('note-card'))
    expect(onPress).toHaveBeenCalledTimes(1)
  })
})

testID

Every Glyx node accepts a testID prop. Use it for stable selectors that don't depend on visible text (icon buttons, containers, form fields):

<Pressable testID="submit-btn" onPress={handleSubmit}>
  <Text>Save</Text>
</Pressable>
// In your test — prefer testID over text for interactive elements
fireEvent.press(screen.getByTestId('submit-btn'))

testID is tracked both in the in-memory reconciler (for JS tests) and in the native Rust node tree (for future integration tests).


Async state

Wrap state updates in act, then use waitFor for anything asynchronous:

import { render, screen, act, waitFor, fireEvent } from '@glyx-dev/testing'
 
test('counter increments', async () => {
  const { getByText } = await render(<Counter initialValue={0} />)
 
  await act(() => {
    fireEvent.press(getByText('+'))
  })
 
  await waitFor(() => {
    expect(screen.getByText('1')).toBeTruthy()
  })
})

Mocking native bindings

Override any __glyx_* binding for a specific test with mockBinding. Call restoreAllBindings in afterEach to prevent test pollution:

import { installStubs, mockBinding, restoreAllBindings, render, waitFor, screen } from '@glyx-dev/testing'
import { afterEach, test, expect } from 'bun:test'
 
afterEach(() => restoreAllBindings())
 
test('displays fetched notes', async () => {
  mockBinding('__glyx_fetch', () =>
    Promise.resolve(JSON.stringify({
      status: 200,
      ok: true,
      body: JSON.stringify([{ id: 1, title: 'My Note' }]),
      headers: { 'content-type': 'application/json' },
    }))
  )
 
  await render(<NotesList />)
  await waitFor(() => expect(screen.getByText('My Note')).toBeTruthy())
})
 
test('handles fetch error', async () => {
  mockBinding('__glyx_fetch', () =>
    Promise.resolve(JSON.stringify({ status: 500, ok: false, body: '', headers: {} }))
  )
 
  await render(<NotesList />)
  await waitFor(() => expect(screen.getByText('Failed to load')).toBeTruthy())
})

Use bun:test's mock() for JS function mocks, and mockBinding() for Glyx native API stubs. They are independent — restoreAllBindings() only resets Glyx bindings, not mock() instances.


Inspecting tree structure

Use debug() to print the node tree during development, or getNodeTree() for structural assertions:

import { render, screen, getNodeTree } from '@glyx-dev/testing'
 
test('shows correct structure', async () => {
  await render(<UserCard user={{ name: 'Alice' }} />)
 
  // Print the tree to console (useful while writing tests)
  screen.debug()
 
  // Assert structure programmatically
  const tree = getNodeTree()
  const cards = [...tree.values()].filter((n) => n.props.testID === 'user-card')
  expect(cards).toHaveLength(1)
})

API summary

ExportDescription
installStubs()Stub all __glyx_* native bindings
render(element)Render a component tree; returns query helpers + unmount
screen.getByText(text)Find node by text content — throws if not found
screen.queryByText(text)Like getByText, returns null if not found
screen.getAllByText(text)All nodes matching text
screen.getByTestId(id)Find node by testID prop — throws if not found
screen.queryByTestId(id)Like getByTestId, returns null if not found
screen.getAllByTestId(id)All nodes matching testID
screen.debug()Print the in-memory node tree
getNodeTree()Return a Map snapshot of the full node tree
act(callback)Flush React state updates before asserting
fireEvent.press(node)Simulate a press — calls node.onPress()
fireEvent.changeText(node, text)Simulate text input
fireEvent.submitEditing(node)Simulate Enter / submit
fireEvent.scroll(node, offset)Simulate a scroll
waitFor(assertion, opts?)Poll until assertion passes or timeout
mockBinding(name, fn)Replace a specific __glyx_* binding
restoreAllBindings()Restore all bindings overridden by mockBinding

See packages/@glyx-dev/testing for the full reference.


Running in CI

# .github/workflows/ci.yml (excerpt)
- name: JS tests
  run: bun test
 
- name: Rust tests
  run: cargo test --workspace
 
# Or use glyx CLI:
- name: All tests
  run: glyx test

glyx test runs both JS (bun test) and Rust (cargo test) and reports failures from both sides before exiting. Pass --js or --rust to run only one side. See CLI reference for details.


Best practices

  • Add testID to every interactive element — selectors that don't depend on display text survive copy changes and translations.
  • Keep business logic in plain functions or hooks. Test those directly without rendering anything.
  • Use waitFor for anything driven by useEffect or async state.
  • Call restoreAllBindings() in afterEach when using mockBinding so mocks don't leak between tests.
  • Prefer getByTestId over getByText for Pressables and inputs; prefer getByText for asserting rendered content.