Skip to content

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:

json
{
  "scripts": {
    "test": "vitest run --config vitest.browser.ts"
  }
}
ts
import viteConfig from './vite.config.ts';
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 ​

tsx
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');
    });
});
tsx
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:

tsx
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);
});
tsx
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:

tsx
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:

tsx
it('should handle click events', async () => {
    const toggle = new Button();
    container.appendChild(toggle);

    await userEvent.click(toggle);

    expect(toggle.on).toBeTruthy();
});
tsx
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:

tsx
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();
});
tsx
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:

tsx
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.

tsx
it('should not have accessibility violations', async () => {
    const button = new Button();
    container.appendChild(button);

    expect(await axe(button)).toHaveNoViolations();
});

Resources ​

Unleash the power of Custom Elements through declarative API, customized built-in elements and shadow-free composition.