Write comprehensive tests for your BYO components using the SDK's testing utilities and Vitest.
The BYO SDK provides the following testing tools:
- Testing utilities: Helpers for mounting, mocking, and asserting.
- Vitest integration: Fast unit test runner with happy-dom environment.
- Mock host: Simulates platform behavior.
- Callback spies: Lightweight assertion helpers.
Getting Started
Scaffolded projects include Vitest and testing utilities preconfigured. The scaffold adds the following script to package.json:
{
"scripts": {
"test": "vitest run"
}
}
To add watch mode, UI mode, and coverage support, add the following scripts manually:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
Run tests using the following commands:
# Run once
npm test
# Watch mode
npm run test:watch
# UI mode
npm run test:ui
# With coverage
npm run test:coverage
Testing Utilities
Import from @unqork/byo-sdk/testing:
import {
mountByoComponent,
applyDefaults,
createCallbackSpy,
defineMockScenario,
} from '@unqork/byo-sdk/testing'
mountByoComponent
Mounts a component export as a custom element in the document.
async function mountByoComponent(
componentExport: ByoComponentExport,
tagPrefix?: string // default: 'test'
): Promise<HTMLElement>
Example:
import { mountByoComponent } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
it('renders a button element', async () => {
const el = await mountByoComponent(myButton)
expect(el.querySelector('button')).toBeTruthy()
})
Each call generates a unique tag name to avoid conflicts between tests.
applyDefaults
Reads default values from the model schema and sets them as properties.
async function applyDefaults(
element: HTMLElement,
componentExport: ByoComponentExport
): Promise<void>
Example:
import { mountByoComponent, applyDefaults } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
it('applies default label', async () => {
const el = await mountByoComponent(myButton) as any
await applyDefaults(el, myButton)
// Model defines: label = 'Click me'
expect(el.label).toBe('Click me')
})
Simulates what the platform does before rendering a component.
createCallbackSpy
Creates a spy function that records all invocations.
function createCallbackSpy(): {
fn: (...args: unknown[]) => void
calls: unknown[][]
}
Example:
import { mountByoComponent, createCallbackSpy } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
it('fires callback on click', async () => {
const el = await mountByoComponent(myButton) as any
const spy = createCallbackSpy()
el.onButtonClicked = spy.fn
el.label = 'Test'
el.querySelector('button')!.click()
expect(spy.calls).toHaveLength(1)
expect(spy.calls[0][0]).toEqual({ label: 'Test' })
})
The spy records calls as an array of argument arrays.
Testing Patterns
Basic Component Test
import { describe, it, expect } from 'vitest'
import { mountByoComponent, applyDefaults } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
describe('MyButton', () => {
it('renders with default props', async () => {
const el = await mountByoComponent(myButton)
await applyDefaults(el, myButton)
expect(el.querySelector('button')?.textContent).toBe('Click me')
})
})
Testing Props
it('updates text when label changes', async () => {
const el = await mountByoComponent(myButton) as any
el.label = 'First'
expect(el.querySelector('button')?.textContent).toBe('First')
el.label = 'Second'
expect(el.querySelector('button')?.textContent).toBe('Second')
})
it('disables button when disabled prop is true', async () => {
const el = await mountByoComponent(myButton) as any
el.disabled = true
expect(el.querySelector('button')?.disabled).toBe(true)
})
Testing Callbacks
import { createCallbackSpy } from '@unqork/byo-sdk/testing'
it('fires onButtonClicked with correct payload', async () => {
const el = await mountByoComponent(myButton) as any
const spy = createCallbackSpy()
el.onButtonClicked = spy.fn
el.label = 'Submit'
el.querySelector('button')!.click()
expect(spy.calls).toHaveLength(1)
expect(spy.calls[0][0]).toEqual({ label: 'Submit' })
})
it('does not error if callback is not set', async () => {
const el = await mountByoComponent(myButton) as any
// Should not throw
expect(() => {
el.querySelector('button')!.click()
}).not.toThrow()
})
Testing Submission Data
it('updates submission data on change', async () => {
const el = await mountByoComponent(myInput) as any
const spy = createCallbackSpy()
el.onSubmissionUpdate = spy.fn
const input = el.querySelector('input')!
input.value = 'test value'
input.dispatchEvent(new Event('input'))
expect(spy.calls).toHaveLength(1)
expect(spy.calls[0][0]).toBe('test value')
})
it('initializes from submissionData prop', async () => {
const el = await mountByoComponent(myInput) as any
el.submissionData = 'initial value'
expect(el.querySelector('input')?.value).toBe('initial value')
})
Testing Complex Interactions
describe('TaskList', () => {
it('toggles task completion', async () => {
const el = await mountByoComponent(taskList) as any
const toggleSpy = createCallbackSpy()
const updateSpy = createCallbackSpy()
el.onTaskToggled = toggleSpy.fn
el.onSubmissionUpdate = updateSpy.fn
el.items = [
{ id: '1', title: 'Task 1', completed: false }
]
const checkbox = el.querySelector('input[type="checkbox"]')!
checkbox.click()
// Check callback was fired
expect(toggleSpy.calls).toHaveLength(1)
expect(toggleSpy.calls[0][0].task.completed).toBe(true)
// Check submission was updated
expect(updateSpy.calls).toHaveLength(1)
expect(updateSpy.calls[0][0][0].completed).toBe(true)
// Check UI updated
const taskEl = el.querySelector('.task')!
expect(taskEl.classList.contains('completed')).toBe(true)
})
})
Testing Error Handling
it('handles invalid prop gracefully', async () => {
const el = await mountByoComponent(myButton) as any
// Should not throw
expect(() => {
el.label = null
}).not.toThrow()
// Should render empty or default
expect(el.querySelector('button')?.textContent).toBe('')
})
it('validates submission data format', async () => {
const el = await mountByoComponent(myInput) as any
const spy = createCallbackSpy()
el.onSubmissionUpdate = spy.fn
// Component should reject invalid data
el.submissionData = { invalid: 'object' }
expect(spy.calls).toHaveLength(0)
})
Testing Framework-Specific Components
React Components
// MyButton.test.tsx
import { describe, it, expect } from 'vitest'
import { mountByoComponent, createCallbackSpy } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
describe('MyButton (React)', () => {
it('renders react component', async () => {
const el = await mountByoComponent(myButton) as any
el.label = 'React Button'
// Wait for React to render
await new Promise(resolve => setTimeout(resolve, 0))
expect(el.shadowRoot?.querySelector('button')?.textContent).toBe('React Button')
})
})
React components use react-to-webcomponent, which renders into a shadow DOM.
Vue Components
// MyButton.test.ts
import { describe, it, expect } from 'vitest'
import { mountByoComponent } from '@unqork/byo-sdk/testing'
import { myButton } from './entry'
describe('MyButton (Vue)', () => {
it('renders vue component', async () => {
const el = await mountByoComponent(myButton) as any
el.label = 'Vue Button'
// Wait for Vue to render
await new Promise(resolve => setTimeout(resolve, 0))
expect(el.querySelector('button')?.textContent).toBe('Vue Button')
})
})
Testing Async Behavior
Debounced Updates
import { vi } from 'vitest'
it('debounces submission updates', async () => {
vi.useFakeTimers()
const el = await mountByoComponent(myInput) as any
const spy = createCallbackSpy()
el.onSubmissionUpdate = spy.fn
const input = el.querySelector('input')!
input.value = 'a'
input.dispatchEvent(new Event('input'))
input.value = 'ab'
input.dispatchEvent(new Event('input'))
input.value = 'abc'
input.dispatchEvent(new Event('input'))
// Fast-forward time
vi.advanceTimersByTime(300)
// Should only call once (debounced)
expect(spy.calls).toHaveLength(1)
expect(spy.calls[0][0]).toBe('abc')
vi.useRealTimers()
})
Lazy Loading
it('lazy loads chart library on demand', async () => {
const el = await mountByoComponent(myChart) as any
// Initially no chart rendered
expect(el.querySelector('.chart')).toBeNull()
// Trigger chart rendering
el.showChart = true
// Wait for dynamic import
await new Promise(resolve => setTimeout(resolve, 100))
expect(el.querySelector('.chart')).toBeTruthy()
})
Coverage
Run tests with coverage:
npm run test:coverage
Output:
Coverage report:
File | % Stmts | % Branch | % Funcs | % Lines
------------------|---------|----------|---------|--------
All files | 92.5 | 88.3 | 95.0 | 93.2
MyButton.ts | 95.0 | 90.0 | 100.0 | 96.0
MyInput.ts | 90.0 | 85.0 | 90.0 | 91.0
To configure coverage thresholds, add the following to your vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
}
}
}
})
Best Practices
1. Test User Interactions, Not Implementation
Bad:
it('sets _label property', () => {
el._label = 'test' // Testing internal state
})
Good:
it('updates button text when label changes', () => {
el.label = 'test'
expect(el.querySelector('button')?.textContent).toBe('test')
})
2. Use Descriptive Test Names
Bad:
it('works', () => { ... })
Good:
it('fires onButtonClicked with label when button is clicked', () => { ... })
3. Test Edge Cases
describe('MyButton edge cases', () => {
it('handles empty label', async () => {
el.label = ''
expect(el.querySelector('button')?.textContent).toBe('')
})
it('handles null label', async () => {
el.label = null as any
expect(el.querySelector('button')?.textContent).toBe('')
})
it('handles very long label', async () => {
el.label = 'x'.repeat(1000)
expect(el.querySelector('button')?.textContent).toHaveLength(1000)
})
})
4. Clean Up After Tests
import { afterEach } from 'vitest'
afterEach(() => {
// Remove mounted elements
document.body.innerHTML = ''
})
5. Test Accessibility
it('has accessible label', async () => {
const el = await mountByoComponent(myButton) as any
el.label = 'Submit form'
const button = el.querySelector('button')!
expect(button.getAttribute('aria-label')).toBe('Submit form')
})
it('supports keyboard navigation', async () => {
const el = await mountByoComponent(myButton) as any
const spy = createCallbackSpy()
el.onButtonClicked = spy.fn
const button = el.querySelector('button')!
button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
expect(spy.calls).toHaveLength(1)
})
Debugging Tests
Enable Verbose Logging
npm test -- --reporter verbose
Inspect the DOM
it('debug test', async () => {
const el = await mountByoComponent(myButton) as any
el.label = 'Test'
console.log(el.outerHTML)
// <test-my-button-1>
// <button>Test</button>
// </test-my-button-1>
})
Use the Vitest UI
npm run test:ui
Opens an interactive browser UI for running and debugging tests.
Next Steps
- Building Components — step-by-step guide.
- CLI Reference — complete command documentation.
- SDK Overview — SDK features and configuration.
Changelog
| Date | Change |
|---|---|
| 2026-05-01 | Initial publication |