Skip to content

Testing Examples

Real-world test code examples from the DocBits component library.

RowSelectionCheckbox Tests

Complete test suite for the row selection checkbox component (42+ tests).

Basic Selection

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

describe('RowSelectionCheckbox - Basic Selection', () => {
  it('renders as unchecked by default', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.exists()).toBe(true)
    expect(checkbox.element).not.toBeChecked()
  })

  it('renders as checked when isSelected=true', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: true
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.element).toBeChecked()
  })

  it('toggles selection on click', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    await checkbox.trigger('click')

    expect(wrapper.emitted('toggle')).toBeTruthy()
    expect(wrapper.emitted('toggle')[0]).toEqual(['1', true])
  })
})

Master Checkbox (Select All)

typescript
describe('RowSelectionCheckbox - Master Checkbox', () => {
  it('shows master checkbox with select all label', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'master',
        isSelected: false,
        isMaster: true
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.exists()).toBe(true)
    expect(wrapper.attributes('aria-label')).toContain('Select all')
  })

  it('shows indeterminate state when partially selected', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'master',
        isSelected: true,
        isMaster: true,
        totalRows: 10,
        selectedCount: 5  // Partially selected
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.element).toHaveProperty('indeterminate', true)
  })

  it('not indeterminate when all selected', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'master',
        isSelected: true,
        isMaster: true,
        totalRows: 10,
        selectedCount: 10  // All selected
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.element).toHaveProperty('indeterminate', false)
  })

  it('emits toggle-all event when master clicked', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'master',
        isSelected: false,
        isMaster: true
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    await checkbox.trigger('click')

    expect(wrapper.emitted('toggle-all')).toBeTruthy()
    expect(wrapper.emitted('toggle-all')[0]).toEqual([true])
  })
})

Accessibility

typescript
describe('RowSelectionCheckbox - Accessibility', () => {
  it('generates correct ARIA label for row selection', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'abc123',
        isSelected: false
      }
    })

    const label = wrapper.attributes('aria-label')
    expect(label).toContain('abc123')
    expect(label).toContain('Select')
  })

  it('supports keyboard navigation with Space key', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    await checkbox.trigger('keydown', { key: ' ' })

    expect(wrapper.emitted('toggle')).toBeTruthy()
  })

  it('supports keyboard navigation with Enter key', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    await checkbox.trigger('keydown', { key: 'Enter' })

    expect(wrapper.emitted('toggle')).toBeTruthy()
  })

  it('is focusable via Tab key', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.attributes('tabindex')).not.toBe('-1')
  })
})

Disabled State

typescript
describe('RowSelectionCheckbox - Disabled State', () => {
  it('disables checkbox when disabled=true', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false,
        disabled: true
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.attributes('disabled')).toBeDefined()
  })

  it('does not emit event when disabled and clicked', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false,
        disabled: true
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    await checkbox.trigger('click')

    expect(wrapper.emitted('toggle')).toBeFalsy()
  })

  it('applies disabled styling', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false,
        disabled: true
      }
    })

    expect(wrapper.classes()).toContain('disabled')
  })
})

BulkActionsMenu Tests

Complete test suite for bulk actions menu component (22+ tests).

Visibility

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

describe('BulkActionsMenu - Visibility', () => {
  it('hides menu when no rows selected', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 0,
        totalRows: 10
      }
    })

    expect(wrapper.find('.bulk-actions-menu').exists()).toBe(false)
  })

  it('shows menu when rows selected', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 2,
        totalRows: 10
      }
    })

    expect(wrapper.find('.bulk-actions-menu').exists()).toBe(true)
  })

  it('shows selection counter badge', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 5,
        totalRows: 10
      }
    })

    const counter = wrapper.find('.selection-counter')
    expect(counter.exists()).toBe(true)
    expect(counter.text()).toContain('5')
  })

  it('displays "X of Y" format in counter', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 3,
        totalRows: 10
      }
    })

    const counter = wrapper.find('.selection-counter')
    expect(counter.text()).toContain('3 of 10')
  })
})

Actions

typescript
describe('BulkActionsMenu - Actions', () => {
  it('emits export event when export clicked', async () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10
      }
    })

    await wrapper.find('[aria-label*="Export"]').trigger('click')
    expect(wrapper.emitted('export')).toBeTruthy()
  })

  it('emits delete event when delete clicked', async () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10
      }
    })

    await wrapper.find('[aria-label*="Delete"]').trigger('click')
    expect(wrapper.emitted('delete')).toBeTruthy()
  })

  it('emits copy-ids event when copy IDs clicked', async () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10
      }
    })

    await wrapper.find('[aria-label*="Copy"]').trigger('click')
    expect(wrapper.emitted('copy-ids')).toBeTruthy()
  })

  it('emits clear-selection event when clear clicked', async () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 5,
        totalRows: 10
      }
    })

    await wrapper.find('[aria-label*="Clear"]').trigger('click')
    expect(wrapper.emitted('clear-selection')).toBeTruthy()
  })
})

Disabled State

typescript
describe('BulkActionsMenu - Disabled State', () => {
  it('disables all actions when disabled=true', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10,
        disabled: true
      }
    })

    const buttons = wrapper.findAll('button')
    buttons.forEach(button => {
      expect(button.attributes('disabled')).toBeDefined()
    })
  })

  it('does not emit events when disabled and clicked', async () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10,
        disabled: true
      }
    })

    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted().length).toBe(0)
  })

  it('shows disabled styling', () => {
    const wrapper = mount(BulkActionsMenu, {
      props: {
        selectedCount: 1,
        totalRows: 10,
        disabled: true
      }
    })

    expect(wrapper.classes()).toContain('disabled')
  })
})

ColumnOrderBy Tests

Sorting component test patterns.

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

describe('ColumnOrderBy - Sorting', () => {
  it('renders up and down arrow buttons', () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: null,
        currentDirection: null
      }
    })

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

  it('highlights up arrow when ascending', () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: 'name',
        currentDirection: 'asc'
      }
    })

    const upArrow = wrapper.find('.up-arrow')
    expect(upArrow.classes()).toContain('selected')
  })

  it('highlights down arrow when descending', () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: 'name',
        currentDirection: 'desc'
      }
    })

    const downArrow = wrapper.find('.down-arrow')
    expect(downArrow.classes()).toContain('selected')
  })

  it('emits sort event with correct direction on arrow click', async () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: null,
        currentDirection: null
      }
    })

    await wrapper.find('.up-arrow').trigger('click')

    expect(wrapper.emitted('sort')).toBeTruthy()
    expect(wrapper.emitted('sort')[0][0]).toEqual({
      column: 'name',
      direction: 'asc'
    })
  })

  it('does not emit when clicking already selected arrow', async () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: 'name',
        currentDirection: 'asc'
      }
    })

    const selectedArrow = wrapper.find('.up-arrow.selected')
    await selectedArrow.trigger('click')

    expect(wrapper.emitted('sort')).toBeFalsy()
  })

  it('handles rapid clicks correctly', async () => {
    const wrapper = mount(ColumnOrderBy, {
      props: {
        column: 'name',
        currentSort: null,
        currentDirection: null
      }
    })

    const upArrow = wrapper.find('.up-arrow')

    // Rapid clicks
    await upArrow.trigger('click')
    await upArrow.trigger('click')
    await upArrow.trigger('click')

    expect(wrapper.emitted('sort')).toBeTruthy()
    // Should still work despite rapid clicks
  })
})

DocBitsTable Integration Tests

Complete workflow tests for the main table component.

typescript
describe('DocBitsTable - Complete Workflows', () => {
  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' },
          { name: 'name', label: 'Name', field: 'name' }
        ],
        rowKey: 'id',
        features: {
          selection: true,
          bulkActions: true
        }
      }
    })

    // Step 1: Select first row
    const firstCheckbox = wrapper.find('input[type="checkbox"]')
    await firstCheckbox.trigger('click')

    expect(wrapper.emitted('selection-change')).toBeTruthy()
    const selectionEvent = wrapper.emitted('selection-change')[0][0] as any
    expect(selectionEvent.selected.size).toBe(1)

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

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

    expect(wrapper.emitted('bulk-action')).toBeTruthy()
    const actionEvent = wrapper.emitted('bulk-action')[0][0] as any
    expect(actionEvent.action).toBe('delete')
  })

  it('handles row sorting with ColumnOrderBy', async () => {
    const rows = [
      { id: 3, name: 'Charlie' },
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]

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

    // Click sort on name column
    const sortButton = wrapper.find('[aria-label*="Sort"]')
    await sortButton.trigger('click')

    expect(wrapper.emitted('update:sorting')).toBeTruthy()
  })

  it('handles pagination state changes', async () => {
    const rows = Array.from({ length: 25 }, (_, i) => ({
      id: i + 1,
      name: `Item ${i + 1}`
    }))

    const wrapper = mount(DocBitsTable, {
      props: {
        rows,
        columns: [
          { name: 'id', label: 'ID', field: 'id' },
          { name: 'name', label: 'Name', field: 'name' }
        ],
        rowKey: 'id',
        pagination: {
          page: 1,
          rowsPerPage: 10,
          rowsNumber: 25
        },
        features: { pagination: true }
      }
    })

    // Find and click next page button
    const nextButton = wrapper.find('[aria-label*="Next"]')
    if (nextButton.exists()) {
      await nextButton.trigger('click')
      expect(wrapper.emitted('update:pagination')).toBeTruthy()
    }
  })
})

Async Operations Tests

Testing components with async data loading.

typescript
describe('DocBitsTable - Async Operations', () => {
  it('shows loading state while data fetches', async () => {
    const wrapper = mount(DocBitsTable, {
      props: {
        rows: [],
        columns: [{ name: 'id', label: 'ID', field: 'id' }],
        loading: false
      }
    })

    // No spinner initially
    expect(wrapper.find('.loading-spinner').exists()).toBe(false)

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

    // Data arrives
    await wrapper.setProps({
      loading: false,
      rows: [{ id: 1 }]
    })
    expect(wrapper.find('.loading-spinner').exists()).toBe(false)
    expect(wrapper.find('tbody tr').exists()).toBe(true)
  })

  it('handles async data updates correctly', 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)

    // Simulate async data update
    await wrapper.setProps({ rows: updatedRows })
    expect(wrapper.findAll('tbody tr')).toHaveLength(2)
  })
})

Running These Examples

Copy any of these test suites into your component's .spec.ts file:

bash
# Run the tests
npm run test

# Run with coverage
npm run test:coverage

# Run in watch mode
npm run test:watch

# Run specific test file
npm run test -- RowSelectionCheckbox.spec.ts

Best Practices Applied

All examples follow these patterns:

  1. Arrange-Act-Assert - Setup, execute, verify
  2. Descriptive Names - Test names clearly state what is tested
  3. Single Responsibility - Each test verifies one behavior
  4. Mock When Needed - Async operations mocked, not real API calls
  5. Accessibility First - Tests use semantic selectors and ARIA attributes
  6. User Perspective - Tests verify user-facing behavior

More Examples

DocBits Component Library