Testing
DNA components are Web Components that follow the Custom Elements specification, making them highly testable with standard tools. This guide covers how to test DNA components using Vitest with browser automation.
Setup
DNA's test suite is configured with Vitest in browser mode, which runs tests in a real browser environment using Playwright. This ensures your components work exactly as they will in production.
Installation
Your project comes pre-configured with Vitest 4 for browser testing. The relevant configuration is:
{
"scripts": {
"test": "vitest run --config vitest.browser.ts"
}
}import viteConfig from './vite.config'
import { defineConfig, mergeConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [
{ browser: 'chromium' },
{ browser: 'webkit' },
{ browser: 'firefox' }
]
}
}
})
);Unit Tests
Unit tests verify the behavior of your component in isolation, including property changes, state management, and rendering logic.
Basic Component Test
import { customElement, HTML } from '@chialab/dna';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Button } from './x-button';
describe('Button Component', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('should render the button', () => {
const button = new Button();
container.appendChild(button);
expect(button.textContent).toBe('Click me');
});
});import { customElement, HTML } from '@chialab/dna';
@customElement('x-button', {
extends: 'button',
})
export class Button extends HTML.Button {
render() {
return 'Click me';
}
}Testing Properties
Test that properties are correctly synced with attributes and trigger re-renders:
it('should update when property changes', async () => {
const observer = vi.fn();
const counter = new Counter();
counter.observe('count', observer);
container.appendChild(counter);
expect(counter.textContent).toBe('0');
expect(counter.count).toBe(0);
expect(observer).not.toHaveBeenCalled();
counter.count = 5;
expect(counter.textContent).toBe('5');
expect(counter.count).toBe(5);
expect(observer).toHaveBeenCalledWith(0, 5);
});import { customElement, Component, property } from '@chialab/dna';
@customElement('x-counter')
export class Counter extends Component {
@property() count = 0;
render() {
return this.count;
}
}Interaction Tests
For integration testing, use userEvent from Vitest's browser module to simulate real user interactions like clicks, typing, and form submissions.
Importing userEvent
In Vitest 4 browser mode, userEvent is available through the vitest/browser module:
import { describe, beforeAll, beforeEach, afterEach, test, expect } from 'vitest';
import { userEvent } from 'vitest/browser';
describe('User Interactions', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
// Your tests here
});Clicking Elements
Test toggle click interactions:
it('should handle click events', async () => {
const toggle = new Button();
container.appendChild(toggle);
await userEvent.click(toggle);
expect(toggle.on).toBeTruthy();
});import { customElement, HTML, property, listen } from '@chialab/dna';
@customElement('x-toggle')
export class Button extends HTML.Button {
@property({ type: Boolean }) on = false;
@listen('click')
toggle() {
this.on = !this.on;
}
render() {
return this.on ? 'ON' : 'OFF';
}
}Testing Custom Events
Verify that components dispatch and handle custom events:
it('should dispatch custom events', async () => {
const field = new InputField();
container.appendChild(field);
const callback = vi.fn();
field.addEventListener('change', callback);
await userEvent.type(input, 'hello');
expect(callback).toHaveBeenCalled();
});import { customElement, Component, property, listen } from '@chialab/dna';
@customElement('x-input-field')
export class InputField extends Component {
render() {
return <input type="text" class="text-field" />;
}
}Best Practices
Use beforeEach and afterEach
Always clean up the DOM between tests:
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});Test Accessibility
Ensure your components are accessible using the aXe plugin for Vitest.
it('should not have accessibility violations', async () => {
const button = new Button();
container.appendChild(button);
expect(await axe(button)).toHaveNoViolations();
});