Skip to content

Testing Guide

Comprehensive testing documentation for DocBits components with Vitest, Vue Test Utils, and Playwright.

Testing Strategy

DocBits components are thoroughly tested with three levels of testing:

1. Unit Tests (Low Level)

Test individual functions and component logic in isolation.

  • Focus: Pure functions, utilities, component logic
  • Tools: Vitest, Jest assertions
  • Coverage: >95% line coverage
  • Speed: Fast (milliseconds)
  • Example: Testing sort direction toggle logic
typescript
it('toggles direction from asc to desc', () => {
  const direction = 'asc'
  const newDirection = direction === 'asc' ? 'desc' : 'asc'
  expect(newDirection).toBe('desc')
})

2. Component Tests (Mid Level)

Test components working together with realistic user interactions.

  • Focus: Component integration, user workflows, features
  • Tools: Vitest, Vue Test Utils, @vue/test-utils
  • Coverage: 40+ tests per major component
  • Speed: Fast (seconds)
  • Example: Testing row selection with checkbox
typescript
it('selects row when checkbox clicked', async () => {
  const wrapper = mount(RowSelectionCheckbox, {
    props: { rowId: '1', isSelected: false }
  })
  await wrapper.find('input').trigger('click')
  expect(wrapper.emitted('toggle')).toBeTruthy()
})

3. E2E Tests (High Level)

Test complete user workflows in a browser environment.

  • Focus: Full application flows, user journeys
  • Tools: Playwright
  • Coverage: Critical paths
  • Speed: Slower (seconds to minutes)
  • Example: Login → Select rows → Perform bulk action → Verify result

Test Statistics

Our components include:

  • 100+ Unit Tests - Core functionality and edge cases
  • 40+ Component Tests - Integration and workflows
  • 30+ Test Files - Organized by component and feature

Coverage Requirements

All DocBits components maintain:

  • Line Coverage: >95%
  • Branch Coverage: >90%
  • Function Coverage: >95%

Testing Documentation

Unit Testing

Detailed patterns and examples for unit testing:

  • Props validation
  • Event emission
  • User interactions
  • Computed properties
  • Edge cases
  • Running tests with coverage

Component Testing

Patterns for testing component integration:

  • Component integration scenarios
  • Feature flags and toggles
  • Complete user workflows
  • Async operations
  • Accessibility testing
  • Dark mode verification

Examples

Real test code from the DocBits component library:

  • RowSelectionCheckbox test suite (42 tests)
  • BulkActionsMenu test suite (22 tests)
  • Integration test examples
  • Async operation patterns
  • Copy-paste ready examples

Quick Start

Setup

bash
# Install test dependencies
npm install -D vitest @vue/test-utils jsdom

# Create vite.config.ts with test configuration
# Create test files alongside components

Run Tests

bash
# Run all tests
npm run test

# Run specific test file
npm run test -- src/components/Table/RowSelectionCheckbox.spec.ts

# Run tests in watch mode
npm run test:watch

# Run with coverage report
npm run test:coverage

Basic Test Structure

typescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'

describe('MyComponent', () => {
  it('renders component', () => {
    const wrapper = mount(MyComponent, {
      props: { /* props */ }
    })
    expect(wrapper.exists()).toBe(true)
  })

  it('emits event on interaction', async () => {
    const wrapper = mount(MyComponent, {
      props: { /* props */ }
    })
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

Best Practices

1. Test User Behavior, Not Implementation

typescript
// ✅ Good: Tests what user sees
it('shows selected arrow in primary color', () => {
  const wrapper = mount(ColumnOrderBy, {
    props: { column: 'name', currentSort: 'name', currentDirection: 'asc' }
  })
  const arrow = wrapper.find('.up-arrow.selected')
  expect(arrow.exists()).toBe(true)
})

// ❌ Bad: Tests internal state
it('sets internal state.selectedDirection to asc', () => {
  const wrapper = mount(ColumnOrderBy, { /* ... */ })
  expect(wrapper.vm.state.selectedDirection).toBe('asc')
})

2. Use Arrange-Act-Assert Pattern

typescript
it('handles row selection workflow', () => {
  // Arrange: Set up initial state
  const wrapper = mount(RowSelectionCheckbox, {
    props: { rowId: '1', isSelected: false }
  })

  // Act: Perform user action
  const checkbox = wrapper.find('input')
  await checkbox.trigger('click')

  // Assert: Verify result
  expect(wrapper.emitted('toggle')).toBeTruthy()
  expect(wrapper.emitted('toggle')[0]).toEqual(['1', true])
})

3. Test Edge Cases

typescript
// Empty/null values
it('handles null input', () => { /* */ })

// Boundary conditions
it('handles maximum values', () => { /* */ })

// Special characters
it('handles special characters in input', () => { /* */ })

// Rapid interactions
it('handles rapid clicks', async () => {
  await element.trigger('click')
  await element.trigger('click')
  await element.trigger('click')
  expect(wrapper.emitted('click')).toHaveLength(3)
})

4. Test Complete Workflows

typescript
it('completes select and delete workflow', async () => {
  const wrapper = mount(DocBitsTable, { /* ... */ })

  // Step 1: Select row
  const checkbox = wrapper.find('input[type="checkbox"]')
  await checkbox.trigger('click')
  expect(wrapper.emitted('selection-change')).toBeTruthy()

  // Step 2: Verify bulk menu appears
  expect(wrapper.find('.bulk-actions-menu').exists()).toBe(true)

  // Step 3: Trigger delete
  await wrapper.find('[aria-label*="Delete"]').trigger('click')
  expect(wrapper.emitted('bulk-action')).toBeTruthy()
})

Debugging Tests

View Component HTML

typescript
it('renders correctly', () => {
  const wrapper = mount(Component, { props: { /* ... */ } })
  console.log(wrapper.html())  // Print full rendered HTML
})

Check Event Emissions

typescript
it('emits events', async () => {
  const wrapper = mount(Component, { props: { /* ... */ } })
  await wrapper.find('button').trigger('click')
  console.log(wrapper.emitted())  // Print all emitted events
})

Inspect Component State

typescript
it('updates state', async () => {
  const wrapper = mount(Component, { props: { /* ... */ } })
  console.log(wrapper.vm.$data)  // Component state before

  await wrapper.find('button').trigger('click')

  console.log(wrapper.vm.$data)  // Component state after
})

Wait for Updates

typescript
// Vue reactivity updates
await wrapper.vm.$nextTick()

// DOM updates after async
await new Promise(r => setTimeout(r, 0))

// Await prop changes
await wrapper.setProps({ loading: true })

Test File Organization

src/
├── components/
│   ├── Table/
│   │   ├── ColumnOrderBy.vue
│   │   ├── ColumnOrderBy.spec.ts        # Unit tests
│   │   ├── RowSelectionCheckbox.vue
│   │   ├── RowSelectionCheckbox.spec.ts
│   │   └── ...
│   └── ...
└── tests/
    ├── unit/
    │   ├── sorting.unit.spec.ts
    │   └── selection.unit.spec.ts
    ├── component/
    │   ├── table-integration.spec.ts
    │   └── workflows.spec.ts
    └── e2e/
        ├── selection-workflow.spec.ts
        └── bulk-actions.spec.ts

Continuous Integration

Tests run automatically on every commit:

yaml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npm run test -- --coverage
      - run: npm run lint

Coverage Goals

Target coverage by component:

ComponentLineBranchFunction
ColumnOrderBy>98%>95%100%
RowSelectionCheckbox>97%>95%100%
BulkActionsMenu>96%>90%>98%
RowActionsMenu>95%>90%>95%
DocBitsTable>95%>85%>95%
UnifiedTable>94%>85%>94%

Common Testing Issues

"Element not found" errors

Cause: Component hasn't rendered yet

Solution: Wait for Vue updates

typescript
await wrapper.vm.$nextTick()

Event not emitted

Cause: Wrong selector or trigger type

Solution: Verify element exists first

typescript
expect(wrapper.find('button').exists()).toBe(true)
await wrapper.find('button').trigger('click')

Props not updating

Cause: Missing await on setProps

Solution: Always await prop changes

typescript
await wrapper.setProps({ disabled: true })

Tests timeout

Cause: Waiting for async operation that never completes

Solution: Add reasonable timeout or mock the async operation

typescript
it('loads data', async () => {
  vi.mock('@/api', () => ({
    fetchData: vi.fn(() => Promise.resolve([]))
  }))
  // ... test code
}, { timeout: 5000 })

Resources

DocBits Component Library