Testing
Test WildflowerJS components with confidence using our official test utilities package.
@wildflowerjs/test-utils
The official testing utilities package provides everything you need to test WildflowerJS applications:
- Framework loading - Load source or distribution builds
- State isolation - Clean slate between tests
- Timing utilities - Wait for reactive updates correctly
- Vitest integration - Automatic setup with hooks
- TypeScript support - Full type definitions included
Installation
npm install @wildflowerjs/test-utils --save-dev
Quick Start
The Easy Way: setupWildflowerTests()
For most tests, use the automatic setup helper:
import { describe, it, expect } from 'vitest'
import { setupWildflowerTests, waitForUpdate } from '@wildflowerjs/test-utils/vitest'
describe('Counter Component', () => {
const { getContainer, getWildflower } = setupWildflowerTests()
it('should increment count when button clicked', async () => {
const wildflower = getWildflower()
const container = getContainer()
// Register component
wildflower.component('counter', {
state: { count: 0 },
increment() {
this.count++
}
})
// Set up DOM
container.innerHTML = `
<div data-component="counter">
<span id="count-display" data-bind="count"></span>
<button data-action="increment">+</button>
</div>
`
// Initialize
wildflower.scan()
await waitForUpdate()
// Assert initial state (use id, not [data-bind]: attributes are stripped after binding)
expect(container.querySelector('#count-display').textContent).toBe('0')
// Trigger action
container.querySelector('button').click()
await waitForUpdate()
// Assert updated state
expect(container.querySelector('#count-display').textContent).toBe('1')
})
})
setupWildflowerTests() automatically handles:
- Loading the framework (
beforeAll) - Resetting state between tests (
beforeEach) - Creating and cleaning up test containers
- Reinitializing the context system
Manual Setup
For more control, set up the test environment manually:
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'
import {
loadFramework,
resetFramework,
waitForUpdate,
waitForCompleteRender,
createTestContainer,
initContextSystem
} from '@wildflowerjs/test-utils'
describe('My Component', () => {
let container, cleanup
beforeAll(async () => {
await loadFramework()
})
beforeEach(() => {
resetFramework()
initContextSystem()
const result = createTestContainer()
container = result.container
cleanup = result.cleanup
})
afterEach(() => {
cleanup()
})
it('should work', async () => {
// Your test here
})
})
resetFramework() AND initContextSystem() in beforeEach. Missing either will cause test pollution or binding failures.
Mounting Components
mountComponent()
Quick way to mount a component for testing:
import { mountComponent } from '@wildflowerjs/test-utils/vitest'
it('should display user name', async () => {
const { instance, element, cleanup } = await mountComponent(
'user-card',
{
state: { name: 'Alice', role: 'Admin' }
},
`<div data-component="user-card">
<h2 data-bind="name"></h2>
<span data-bind="role"></span>
</div>`
)
expect(element.querySelector('h2').textContent).toBe('Alice')
expect(element.querySelector('span').textContent).toBe('Admin')
cleanup()
})
createTestHarness()
Fluent API for building test components:
import { createTestHarness } from '@wildflowerjs/test-utils/vitest'
it('should calculate total', async () => {
const { instance, element } = await createTestHarness('calculator')
.withState({ price: 100, quantity: 2 })
.withComputed({
total() {
return this.price * this.quantity
}
})
.withTemplate(`
<div data-component="calculator">
<span data-bind="total"></span>
</div>
`)
.mount()
expect(element.querySelector('span').textContent).toBe('200')
})
Timing Utilities
WildflowerJS uses reactive updates that happen asynchronously. Always wait after state changes:
| Utility | When to Use |
|---|---|
waitForUpdate() |
After state changes, clicks, input events |
waitForCompleteRender() |
After scan(), initial render |
waitForState(instance, 'path', value) |
Waiting for async operations to complete |
import { waitForUpdate, waitForCompleteRender, waitForState } from '@wildflowerjs/test-utils'
// After state change
instance.count++
await waitForUpdate()
// After scanning for components
wildflower.scan()
await waitForCompleteRender()
// Waiting for async operation
instance.fetchData() // Sets loading = true, then false
await waitForState(instance, 'loading', false, 2000)
Testing Patterns
Testing State Changes
it('should update display when state changes', async () => {
const { getContainer, getWildflower } = setupWildflowerTests()
const wildflower = getWildflower()
const container = getContainer()
wildflower.component('greeting', {
state: { message: 'Hello' }
})
container.innerHTML = `
<div data-component="greeting">
<span data-bind="message"></span>
</div>
`
wildflower.scan()
await waitForCompleteRender()
// Get instance
const element = container.querySelector('[data-component="greeting"]')
const instance = wildflower.componentInstances.get(element.dataset.componentId)
// Verify initial state
expect(container.querySelector('span').textContent).toBe('Hello')
// Change state
instance.message = 'Goodbye'
await waitForUpdate()
// Verify DOM updated
expect(container.querySelector('span').textContent).toBe('Goodbye')
})
Testing Lists
it('should render and update list items', async () => {
const { instance, element } = await mountComponent(
'todo-list',
{
state: {
items: [
{ text: 'First' },
{ text: 'Second' }
]
},
addItem(text) {
this.items.push({ text })
}
},
`<div data-component="todo-list">
<ul data-list="items">
<template>
<li data-bind="text"></li>
</template>
</ul>
</div>`
)
// Initial render
expect(element.querySelectorAll('li').length).toBe(2)
// Add item
instance.addItem('Third')
await waitForUpdate()
expect(element.querySelectorAll('li').length).toBe(3)
expect(element.querySelectorAll('li')[2].textContent).toBe('Third')
})
Testing Conditionals
it('should toggle visibility with data-show', async () => {
const { instance, element } = await mountComponent(
'toggle',
{ state: { isVisible: false } },
`<div data-component="toggle">
<div data-show="isVisible" class="content">Content</div>
</div>`
)
const content = element.querySelector('.content')
// Initially hidden
expect(content.style.display).toBe('none')
// Show it
instance.isVisible = true
await waitForUpdate()
expect(content.style.display).not.toBe('none')
})
Testing Form Inputs
it('should two-way bind with data-model', async () => {
const { instance, element } = await mountComponent(
'form-test',
{ state: { username: '' } },
`<div data-component="form-test">
<input type="text" data-model="username">
<span data-bind="username"></span>
</div>`
)
const input = element.querySelector('input')
const display = element.querySelector('span')
// Simulate user typing
input.value = 'john_doe'
input.dispatchEvent(new Event('input', { bubbles: true }))
await waitForUpdate()
// State and display should update
expect(instance.username).toBe('john_doe')
expect(display.textContent).toBe('john_doe')
})
Testing Stores
it('should share state via stores', async () => {
const wildflower = getWildflower()
// Create store
wildflower.store('userStore', {
state: { name: 'Guest' },
setName(name) {
this.name = name
}
})
// Component using store
const { element } = await mountComponent(
'user-display',
{
state: {},
external() {
return { user: 'store:userStore' }
}
},
`<div data-component="user-display">
<span data-bind="external:user.name"></span>
</div>`
)
expect(element.querySelector('span').textContent).toBe('Guest')
// Update store
const store = wildflower.storeManager.getStore('userStore')
store.setName('Alice')
await waitForUpdate()
expect(element.querySelector('span').textContent).toBe('Alice')
})
Testing Different Builds
Test your code against different framework builds:
# Test against minified core build
WILDFLOWER_DIST=core npx vitest run
# Test against lite build (fewer features)
WILDFLOWER_DIST=lite npx vitest run
# Test against full build with SSR
WILDFLOWER_DIST=full npx vitest run
Use feature detection to skip tests that require unavailable features:
import { hasFeature, skipIfNoFeature } from '@wildflowerjs/test-utils'
it('should use portals', skipIfNoFeature('portals', async () => {
// Only runs if portals are available (not in lite build)
}))
// Or check manually
if (hasFeature('ssr')) {
// Test SSR functionality
}
| Build Mode | Features |
|---|---|
source |
All features (default) |
core |
Basic reactivity only |
lite |
No portals, transitions, modals |
spa |
Core + Router |
full |
All features including SSR |
Common Pitfalls
Forgetting to Wait
// WRONG - No wait after state change
instance.count++
expect(element.textContent).toBe('1') // Fails!
// CORRECT
instance.count++
await waitForUpdate()
expect(element.textContent).toBe('1')
Missing initContextSystem()
// WRONG - Bindings won't work
beforeEach(() => {
resetFramework()
// Missing initContextSystem()!
})
// CORRECT
beforeEach(() => {
resetFramework()
initContextSystem()
})
Wrong Wait After Component Scan
// WRONG - May not be enough time
wildflower.scan()
await waitForUpdate()
// CORRECT
wildflower.scan()
await waitForCompleteRender()
AI Testing Guide
The test-utils package includes a comprehensive AI Testing Guide (AI_TESTING_GUIDE.md) designed to help AI assistants generate high-quality tests. It includes:
- Standard test file templates
- 15+ "How to Test X" recipes
- Timing utilities cheat sheet
- Common pitfalls with solutions
- Assertion best practices
- Quick reference card
When asking an AI assistant to write tests for your WildflowerJS components, reference this guide for idiomatic, working tests.
API Reference
Core Utilities
| Function | Description |
|---|---|
loadFramework(options?) |
Load the framework (source or dist build) |
resetFramework() |
Clear all component definitions and instances |
initContextSystem() |
Reinitialize the context system after reset |
waitForUpdate(ms?) |
Wait for reactive updates (default 50ms) |
waitForCompleteRender() |
Wait for full render cycle including microtasks |
createTestContainer(options?) |
Create isolated test container element |
getComponent(target) |
Get component instance by name or element |
triggerAction(element, eventType?) |
Trigger action on element (default: click) |
waitForState(instance, path, expected, timeout?) |
Wait for specific state value |
Vitest Integration
| Function | Description |
|---|---|
setupWildflowerTests(options?) |
Auto-setup with beforeAll/beforeEach/afterEach hooks |
mountComponent(name, definition, template) |
Quick component mounting for tests |
createTestHarness(name) |
Fluent API for building test components |
Build Detection
| Function | Description |
|---|---|
getDistMode() |
Get current distribution mode |
hasFeature(feature) |
Check if feature is available in current build |
isMinifiedBuild() |
Check if testing minified build |
skipIfNoFeature(feature, testFn) |
Skip test if feature unavailable |
Vitest Configuration
Browser Mode (Recommended)
For accurate DOM behavior, use Vitest's browser mode:
// vitest.browser.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
headless: true
},
include: ['test/**/*.test.js']
}
})
jsdom (Faster, Less Accurate)
For faster tests that don't need full browser APIs:
// vitest.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom',
include: ['test/**/*.test.js']
}
})
Running Tests
# Run all tests
npx vitest run
# Watch mode
npx vitest
# Specific file
npx vitest run test/counter.test.js
# Pattern matching
npx vitest run -t "should increment"
# With coverage
npx vitest run --coverage
# Verbose output
npx vitest run --reporter=verbose