Skip to content

Component Testing

Component testing guide for DocBits table components.

Overview

Component tests verify that components work correctly in realistic scenarios with multiple interactions.

Coverage:

  • 42+ component tests for RowSelectionCheckbox
  • 22+ component tests for BulkActionsMenu
  • Integration tests for all components

Testing Setup

Same as Unit Testing.

Testing Pattern: Component Integration

Test multiple components working together:

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

describe('DocBitsTable - Component Integration', () => {
  const columns = [
    { name: 'id', label: 'ID', field: 'id' },
    { name: 'name', label: 'Name', field: 'name' }
  ]

  const rows = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]

  it('renders table with headers and data', () => {
    const wrapper = mount(DocBitsTable, {
      props: { rows, columns, rowKey: 'id' }
    })

    // Verify table structure
    expect(wrapper.find('table').exists()).toBe(true)
    expect(wrapper.findAll('th')).toHaveLength(2)
    expect(wrapper.findAll('tbody tr')).toHaveLength(2)
  })

  it('handles row click events', async () => {
    const wrapper = mount(DocBitsTable, {
      props: { rows, columns, rowKey: 'id' }
    })

    const firstRow = wrapper.find('tbody tr')
    await firstRow.trigger('click')

    expect(wrapper.emitted('row-click')).toBeTruthy()
  })
})

Testing Pattern: Feature Flags

Test components with different feature combinations:

typescript
describe('DocBitsTable - Feature Toggles', () => {
  it('shows sorting when enabled', () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: [{ name: 'id', label: 'ID', field: 'id', sortable: true }],
        features: { sorting: true }
      }
    })

    expect(wrapper.find('.sort-arrow').exists()).toBe(true)
  })

  it('hides sorting when disabled', () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: [{ name: 'id', label: 'ID', field: 'id' }],
        features: { sorting: false }
      }
    })

    expect(wrapper.find('.sort-arrow').exists()).toBe(false)
  })

  it('shows pagination when enabled', () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: [],
        features: { pagination: true }
      }
    })

    expect(wrapper.find('.pagination').exists()).toBe(true)
  })
})

Testing Pattern: User Workflows

Test realistic user workflows:

typescript
describe('DocBitsTable - User Workflow: Select and Delete', () => {
  it('completes select and delete workflow', async () => {
    const rows = [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' }
    ]

    const wrapper = mount(DocBitsTable, {
      props: {
        rows,
        columns: [{ name: 'id', label: 'ID', field: 'id' }],
        rowKey: 'id',
        features: { selection: true, bulkActions: true }
      }
    })

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

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

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

Testing Pattern: Async Operations

Test async operations like data loading:

typescript
describe('DocBitsTable - Async Operations', () => {
  it('handles loading state', async () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: [],
        loading: false
      }
    })

    // Initially no spinner
    expect(wrapper.find('.loading-spinner').exists()).toBe(false)

    // Update to loading
    await wrapper.setProps({ loading: true })
    expect(wrapper.find('.loading-spinner').exists()).toBe(true)

    // Update to done
    await wrapper.setProps({ loading: false })
    expect(wrapper.find('.loading-spinner').exists()).toBe(false)
  })

  it('handles async data updates', async () => {
    const initialRows = [{ id: 1, name: 'Item 1' }]
    const updatedRows = [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ]

    const wrapper = mount(DocBitsTable, {
      props: {
        rows: initialRows,
        columns: [{ name: 'id', label: 'ID', field: 'id' }],
        rowKey: 'id'
      }
    })

    expect(wrapper.findAll('tbody tr')).toHaveLength(1)

    // Update rows
    await wrapper.setProps({ rows: updatedRows })
    expect(wrapper.findAll('tbody tr')).toHaveLength(2)
  })
})

Testing Pattern: Accessibility

Test accessibility features:

typescript
describe('DocBitsTable - Accessibility', () => {
  it('has accessible table structure', () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [{ id: 1, name: 'Item' }],
        columns: [
          { name: 'id', label: 'ID', field: 'id' },
          { name: 'name', label: 'Name', field: 'name' }
        ],
        rowKey: 'id'
      }
    })

    // Check for proper table roles
    expect(wrapper.find('table').exists()).toBe(true)
    expect(wrapper.find('thead').exists()).toBe(true)
    expect(wrapper.find('tbody').exists()).toBe(true)
  })

  it('has keyboard navigation support', async () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [{ id: 1 }, { id: 2 }],
        columns: [{ name: 'id', label: 'ID', field: 'id' }],
        rowKey: 'id',
        features: { selection: true }
      }
    })

    // Tab to checkbox
    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.exists()).toBe(true)

    // Space to select
    await checkbox.trigger('keydown', { key: ' ' })
    expect(wrapper.emitted('selection-change')).toBeTruthy()
  })
})

Testing Pattern: Dark Mode

Test component behavior in dark mode:

typescript
describe('DocBitsTable - Dark Mode', () => {
  it('applies correct colors in dark mode', () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: []
      },
      attachTo: document.body
    })

    // Apply dark mode
    document.body.classList.add('dark')

    const styles = window.getComputedStyle(wrapper.element)
    expect(styles.backgroundColor).toBe('rgb(26, 26, 26)') // #1a1a1a
  })
})

Best Practices

1. Test User-Facing Behavior

typescript
// ✅ Good: Tests what user experiences
it('selects row when checkbox clicked', async () => {
  const wrapper = mount(RowSelectionCheckbox, { props: { ... } })
  await wrapper.find('input').trigger('click')
  expect(wrapper.emitted('toggle')).toBeTruthy()
})

// ❌ Bad: Tests internal state
it('updates internal selectionState', async () => {
  const wrapper = mount(RowSelectionCheckbox, { props: { ... } })
  await wrapper.find('input').trigger('click')
  expect(wrapper.vm.internalState).toBe(true)
})

2. Test Complete Workflows

typescript
// ✅ Good: Complete workflow
it('adds, selects, and exports rows', async () => {
  // Setup
  const wrapper = mount(Table, { props: { ... } })

  // Add row
  await wrapper.find('[aria-label="Add"]').trigger('click')

  // Select row
  const checkbox = wrapper.find('input')
  await checkbox.trigger('click')

  // Export
  const exportBtn = wrapper.find('[aria-label="Export"]')
  await exportBtn.trigger('click')

  expect(wrapper.emitted('bulk-action')).toBeTruthy()
})

3. Mock External Dependencies

typescript
// Mock API calls
vi.mock('@/api', () => ({
  fetchRows: vi.fn(() => Promise.resolve([{ id: 1 }]))
}))

it('loads data on mount', async () => {
  const wrapper = mount(DocBitsTable)
  await wrapper.vm.$nextTick()
  expect(fetchRows).toHaveBeenCalled()
})

Common Issues

Issue: "Element not found" errors

Cause: Component hasn't rendered yet

Solution: Wait for updates with nextTick:

typescript
await wrapper.vm.$nextTick()  // Wait for Vue updates
await new Promise(r => setTimeout(r, 0))  // Wait for next tick

Issue: Event not emitted

Cause: Wrong selector or trigger

Solution: Verify element exists and trigger correctly:

typescript
// First verify element exists
expect(wrapper.find('button').exists()).toBe(true)

// Then trigger with correct event
await wrapper.find('button').trigger('click')

// Check emission
expect(wrapper.emitted('click')).toBeTruthy()

Issue: Props not updating

Cause: Need to await setProps

Solution: Always await prop changes:

typescript
await wrapper.setProps({ loading: true })
expect(wrapper.find('.spinner').exists()).toBe(true)

Running Component Tests

bash
# Run all component tests
npm run test

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

# Run with coverage
npm run test:coverage

# Watch mode for development
npm run test:watch

DocBits Component Library