Skip to content

Unit Tests

Overview

Unit tests verify individual functions and modules work correctly in isolation. They form the foundation of our testing pyramid and run extremely fast using JSDOM for DOM-related testing.

Location

All unit tests are in test/unit/ with the following structure:

test/unit/
├── config.test.ts       # Configuration merging and defaults
├── dom.test.ts          # DOM element creation (SPAN vs SUP/SUB)
├── engine.test.ts       # Processing engine and TreeWalker
├── patterns.test.ts     # Regex pattern matching
├── processor.test.ts    # Text transformation logic
├── hybrid-elements.test.ts  # Hybrid element approach
└── regression/          # Bug regression tests
    ├── water.test.ts    # H2O vs H1-H6 headers
    └── headers-bug.test.ts  # Header chemical confusion

Running Unit Tests

bash
# Run all unit tests
npx vitest run test/unit

# Run specific file
npx vitest run test/unit/patterns.test.ts

# Watch mode for development
npx vitest watch test/unit

# With coverage
npx vitest run test/unit --coverage

What We Test

Pattern Matching (patterns.test.ts)

Tests all regex patterns for accuracy:

typescript
describe('Trademark pattern', () => {
  it('should match (TM) pattern', () => {
    const pattern = patterns.trademark
    expect('Product(TM)'.match(pattern)).toEqual(['(TM)'])
  })

  it('should not match partial patterns', () => {
    const pattern = patterns.trademark
    expect('(T M)'.match(pattern)).toBeNull()
  })
})

Configuration (config.test.ts)

Tests configuration merging and defaults:

typescript
describe('Config merging', () => {
  it('should merge user config with defaults', () => {
    const config = mergeConfig({
      debug: true,
      cssVariables: { 'tm-top': '-0.5em' }
    })
    
    expect(config.debug).toBe(true)
    expect(config.cssVariables?.['tm-top']).toBe('-0.5em')
    expect(config.performance.debounce).toBe(100) // Default
  })
})

DOM Creation (dom.test.ts)

Tests the hybrid element approach:

typescript
describe('Element creation', () => {
  it('should create SPAN for trademark', () => {
    const element = createSuperscriptElement('™', 'trademark')
    expect(element.tagName).toBe('SPAN')
    expect(element.className).toBe('ss-sup ss-tm')
  })

  it('should create SUP for math', () => {
    const element = createSuperscriptElement('2', 'math')
    expect(element.tagName).toBe('SUP')
    expect(element.className).toBe('ss-sup ss-math')
  })
})

Text Processing (processor.test.ts)

Tests transformation logic:

typescript
describe('Text processor', () => {
  it('should process trademark symbol', () => {
    const result = processMatch('(TM)')
    expect(result).toEqual({
      modified: true,
      parts: [{ type: 'super', content: '™', subtype: 'trademark' }]
    })
  })

  it('should process mathematical notation', () => {
    const result = processMatch('x^2')
    expect(result).toEqual({
      modified: true,
      parts: [
        { type: 'text', content: 'x' },
        { type: 'super', content: '2', subtype: 'math' }
      ]
    })
  })
})

Test Helpers

Unit tests can use helpers from test/helpers/setup.ts:

typescript
import { setupDOM, cleanupDOM, createTestElement } from '../helpers/setup'

beforeEach(() => {
  setupDOM()
})

afterEach(() => {
  cleanupDOM()
})

it('should process element', () => {
  const element = createTestElement('Test (TM)')
  // ... test logic
})

Best Practices

1. Test One Thing

Each test should verify a single behavior:

typescript
// Good ✅
it('should match trademark pattern', () => {
  expect('(TM)'.match(pattern)).toEqual(['(TM)'])
})

// Bad ❌
it('should handle all symbols', () => {
  expect('(TM)'.match(tmPattern)).toEqual(['(TM)'])
  expect('(R)'.match(rPattern)).toEqual(['(R)'])
  expect('(C)'.match(cPattern)).toEqual(['(C)'])
})

2. Use Descriptive Names

Test names should clearly describe what's being tested:

typescript
// Good ✅
it('should create SPAN element for trademark symbols', () => {})

// Bad ❌
it('should work', () => {})

3. Test Edge Cases

Always include edge cases:

typescript
describe('Pattern matching', () => {
  it('should handle empty string', () => {
    expect(''.match(pattern)).toBeNull()
  })

  it('should handle special characters', () => {
    expect('™®©'.match(pattern)).toBeNull()
  })

  it('should handle very long text', () => {
    const longText = 'x'.repeat(10000)
    // Test performance doesn't degrade
  })
})

4. Mock External Dependencies

Keep tests isolated:

typescript
vi.mock('consola', () => ({
  default: {
    debug: vi.fn(),
    error: vi.fn(),
  }
}))

Common Patterns

Testing Regex Patterns

typescript
describe('Pattern: ${patternName}', () => {
  const pattern = createPatterns().patternName

  it.each([
    ['input1', ['expected1']],
    ['input2', ['expected2']],
  ])('should match %s', (input, expected) => {
    expect(input.match(pattern)).toEqual(expected)
  })

  it.each([
    'invalid1',
    'invalid2',
  ])('should not match %s', (input) => {
    expect(input.match(pattern)).toBeNull()
  })
})

Testing Configuration

typescript
describe('Configuration', () => {
  it('should have sensible defaults', () => {
    const config = createDefaultConfig()
    expect(config.performance.debounce).toBeGreaterThan(0)
    expect(config.performance.batchSize).toBeGreaterThan(0)
  })

  it('should validate user input', () => {
    expect(() => mergeConfig({ performance: { debounce: -1 } }))
      .toThrow('Debounce must be positive')
  })
})

Coverage Goals

We aim for:

  • 100% coverage for pattern matching
  • 100% coverage for configuration
  • 95%+ coverage for processing logic
  • 90%+ coverage for DOM utilities

Check coverage with:

bash
npx vitest run test/unit --coverage

Debugging

bash
# Run with Node inspector
node --inspect-brk ./node_modules/.bin/vitest run test/unit

# Use console.log (temporarily)
it('should debug', () => {
  console.log('Debug value:', value)
  expect(value).toBe(expected)
})

# Run single test
npx vitest run test/unit -t "specific test name"

Apache 2.0 License