Testing
On this page 16
stx provides comprehensive testing utilities to ensure your application's reliability. This guide covers unit testing, component testing, and integration testing approaches.
Setup
Test Configuration
Create a bun.test.ts file in your project root:
import { expect, test, describe } from 'bun:test'
import { mount } from '@stacksjs/stx/test-utils'
// Configure test environment
export default {
testMatch: ['**/*.test.ts'],
testTimeout: 5000,
setupFiles: ['./test/setup.ts']
}
Test Utils
stx provides test utilities for component testing:
import { mount, shallowMount } from '@stacksjs/stx/test-utils'
Testing native screens
A native screen (@stacksjs/stx/native) is tested on a fake phone:
mountNativeScreen(code, screen, options) from @stacksjs/stx/native/testing
runs the compiled bundle in a vm against a fake craft and host, and returns
the tree it draws plus press, type, settle, requests and the rest. See
Native Screens: Testing screens.
Testing reactive directives (DOM runtime shim)
The client signals runtime — the engine behind :if / :else / :for /
:show and the x-* directives — is written against real-browser DOM
semantics that happy-dom doesn't fully provide: Node.ELEMENT_NODE is
undefined, and el.attributes is a Map rather than a NamedNodeMap.
Without bridging those, the runtime's processElement bails on every element
and your reactive directives never run in a test.
stx/testing ships a shim that closes both gaps so you can drive a
component's reactive directives end-to-end and assert on the resulting DOM:
import { describe, expect, it } from 'bun:test'
import { generateSignalsRuntimeDev, state } from '@stacksjs/stx'
import { flushEffects, setupStxTestDom, shimAttributes } from '@stacksjs/stx/testing'
describe('Comments list', () => {
// Install Node.nodeType constants + a requestAnimationFrame polyfill once,
// then execute the signals runtime into the test's window.
setupStxTestDom()
// eslint-disable-next-line no-new-func
new Function(generateSignalsRuntimeDev())()
it('shows the skeleton while loading, the list when loaded', async () => {
const loading = state(true)
const items = state<string[]>([])
window.stx._scopes = { comments: { loading, items } }
document.body.innerHTML = `
<section data-stx-scope="comments">
<ul :if="loading()">skeleton</ul>
<div :else-if="items().length === 0">empty</div>
<ul :else>list</ul>
</section>`
shimAttributes(document.body) // after every innerHTML assignment
document.dispatchEvent(new Event('DOMContentLoaded'))
await flushEffects() // drain the runtime's deferred work
expect(document.body.textContent).toContain('skeleton')
})
})
Helpers:
| Export | Purpose |
|---|---|
setupStxTestDom() | Install the Node.*_NODE constants + a requestAnimationFrame polyfill. Call once before running the runtime. Idempotent. |
shimAttributes(root) | Give root and its descendants a NamedNodeMap-shaped attributes accessor (while preserving get/set). Call after each innerHTML write. |
flushEffects(ms?) | Await the runtime's deferred (setTimeout(0)) child processing before asserting. |
Cross-
.set()reactivity (flip a signal, assert the DOM updated) works when a single runtime instance is active. If you run many test files that each execute the runtime IIFE in one process, effect tracking can drift across instances — keep each runtime-driven file focused, or assert the initial-render state, which is always deterministic.
Unit Testing
Testing Functions and Utilities
// utils/format.ts
export function formatPrice(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
// utils/format.test.ts
import { expect, test } from 'bun:test'
import { formatPrice } from './format'
test('formatPrice formats numbers as USD currency', () => {
expect(formatPrice(10)).toBe('$10.00')
expect(formatPrice(10.5)).toBe('$10.50')
expect(formatPrice(1000)).toBe('$1,000.00')
})
Testing TypeScript Code
// types/user.ts
interface User {
id: number
name: string
role: 'admin' | 'user'
}
function isAdmin(user: User): boolean {
return user.role === 'admin'
}
// types/user.test.ts
import { expect, test } from 'bun:test'
test('isAdmin correctly identifies admin users', () => {
const admin = { id: 1, name: 'Admin', role: 'admin' }
const user = { id: 2, name: 'User', role: 'user' }
expect(isAdmin(admin)).toBe(true)
expect(isAdmin(user)).toBe(false)
})
Component Testing
Testing Basic Components
// components/Button.stx
@component('')
interface Props {
type?: 'primary' | 'secondary'
disabled?: boolean
}
<button
class="btn btn-{{ type }}"
x-disabled="disabled"
>
<slot></slot>
</button>
// components/Button.test.ts
import { expect, test } from 'bun:test'
import { mount } from '@stacksjs/stx/test-utils'
import Button from './Button.stx'
test('Button renders correctly', () => {
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: 'Click me'
}
})
expect(wrapper.text()).toBe('Click me')
expect(wrapper.classes()).toContain('btn-primary')
})
test('Button handles disabled state', () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.attributes('disabled')).toBeDefined()
})
Testing Component Events
// components/Counter.stx
@component('')
let count = 0
function increment() {
count++
emit('change', count)
}
<div>
<span>{{ count }}</span>
<button@click="increment">Increment</button>
</div>
// components/Counter.test.ts
import { expect, test } from 'bun:test'
import { mount } from '@stacksjs/stx/test-utils'
import Counter from './Counter.stx'
test('Counter emits change event on increment', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.emitted().change).toBeTruthy()
expect(wrapper.emitted().change[0]).toEqual([1])
})
Integration Testing
Testing Page Components
import { expect, test } from 'bun:test'
import { mount } from '@stacksjs/stx/test-utils'
import UserDashboard from '../pages/UserDashboard.stx'
test('UserDashboard integrates components correctly', async () => {
const wrapper = mount(UserDashboard)
// Test navigation component
expect(wrapper.find('nav').exists()).toBe(true)
// Test user profile component
const profile = wrapper.findComponent({ name: 'UserProfile' })
expect(profile.exists()).toBe(true)
// Test interaction between components
await wrapper.find('[data-test="edit-profile"]').trigger('click')
expect(wrapper.findComponent({ name: 'ProfileEditor' }).exists()).toBe(true)
})
Testing Routing
import { expect, test } from 'bun:test'
import { mount } from '@stacksjs/stx/test-utils'
import { createRouter } from '@stacksjs/stx/router'
import App from '../App.stx'
test('routing works correctly', async () => {
const router = createRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
})
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
// Test navigation
await router.push('/about')
expect(wrapper.findComponent(About).exists()).toBe(true)
})
Best Practices
-
Test Organization
- Keep tests close to components
- Use descriptive test names
- Group related tests
- Mock external dependencies
-
Testing Strategy
- Unit test utilities and functions
- Component test in isolation
- Integration test key workflows
- End-to-end test critical paths
-
Performance
- Mock heavy operations
- Use shallow mounting when possible
- Clean up after tests
- Run tests in parallel
-
Continuous Integration
- Run tests on every commit
- Maintain test coverage
- Monitor test performance
- Automate test runs
Next Steps
- Explore Deployment
- Learn about State Management
- Check out Performance Optimization
- Review Security Guidelines