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/testingAdd the preload to bunfig.toml so stubs are installed before every test file:
# bunfig.toml
[test]
preload = ["@glyx-dev/testing/setup"]Then run:
bun testUnit 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
| Export | Description |
|---|---|
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 testglyx 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
testIDto 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
waitForfor anything driven byuseEffector async state. - Call
restoreAllBindings()inafterEachwhen usingmockBindingso mocks don't leak between tests. - Prefer
getByTestIdovergetByTextfor Pressables and inputs; prefergetByTextfor asserting rendered content.