Testing Strategies

Comprehensive testing strategies for model-based contenteditable editors.

Overview

Testing contenteditable editors is challenging due to browser differences, IME behavior, and DOM complexity. This guide covers testing strategies at different levels.

Testing Challenges

Key challenges in testing contenteditable editors:

  • Browser differences in event firing and DOM behavior
  • IME composition events don't fire consistently across browsers
  • Selection API differences between browsers
  • Mobile browsers have different behavior than desktop
  • Timing issues with async operations and DOM updates

Unit Testing

Test model operations and transformations in isolation.

Model Testing

import { describe, it, expect } from 'vitest';
import { Editor } from './editor';

describe('Model Operations', () => {
  it('should insert text at cursor', () => {
    const editor = new Editor();
    editor.setContent('Hello world');
    editor.setSelection({ start: 5, end: 5 });
    
    editor.insertText(' beautiful');
    
    expect(editor.getContent()).toBe('Hello beautiful world');
  });

  it('should delete selected text', () => {
    const editor = new Editor();
    editor.setContent('Hello world');
    editor.setSelection({ start: 0, end: 5 });
    
    editor.deleteContent();
    
    expect(editor.getContent()).toBe(' world');
  });
});

Operation Testing

describe('Operations', () => {
  it('should apply insert operation', () => {
    const doc = { type: 'doc', children: [] };
    const operation = {
      type: 'insert',
      path: [0],
      node: { type: 'text', text: 'Hello' }
    };
    
    const result = applyOperation(doc, operation);
    
    expect(result.children[0].text).toBe('Hello');
  });

  it('should support undo/redo', () => {
    const editor = new Editor();
    editor.insertText('Hello');
    editor.insertText(' world');
    
    editor.undo();
    expect(editor.getContent()).toBe('Hello');
    
    editor.redo();
    expect(editor.getContent()).toBe('Hello world');
  });
});

Integration Testing

Test DOM synchronization and event handling.

DOM Testing

import { render, screen, fireEvent } from '@testing-library/react';

describe('DOM Synchronization', () => {
  it('should update DOM when model changes', () => {
    const { container } = render(<Editor />);
    const editor = container.querySelector('[contenteditable]');
    
    // Simulate input
    fireEvent.input(editor, {
      target: { textContent: 'Hello world' }
    });
    
    expect(editor.textContent).toBe('Hello world');
  });
});

Event Testing

describe('Event Handling', () => {
  it('should handle beforeinput events', () => {
    const editor = new Editor();
    const element = editor.element;
    
    const handler = vi.fn();
    editor.on('beforeinput', handler);
    
    const event = new InputEvent('beforeinput', {
      inputType: 'insertText',
      data: 'Hello'
    });
    
    element.dispatchEvent(event);
    
    expect(handler).toHaveBeenCalled();
  });
});

IME Testing

IME testing requires special handling due to browser differences.

Composition Testing

describe('IME Composition', () => {
  it('should handle composition events', async () => {
    const editor = new Editor();
    const element = editor.element;
    
    // Simulate composition
    element.dispatchEvent(new CompositionEvent('compositionstart'));
    element.dispatchEvent(new CompositionEvent('compositionupdate', { data: '한' }));
    element.dispatchEvent(new CompositionEvent('compositionupdate', { data: '한글' }));
    element.dispatchEvent(new CompositionEvent('compositionend', { data: '한글' }));
    
    await new Promise(resolve => setTimeout(resolve, 100));
    
    expect(editor.getContent()).toContain('한글');
  });
});

Cross-Browser IME Testing

Note: iOS Safari doesn't fire composition events for Korean IME. Test with real devices or browser automation tools.

End-to-End Testing

Test full user workflows with browser automation.

Playwright Testing

import { test, expect } from '@playwright/test';

test('should insert text', async ({ page }) => {
  await page.goto('/editor');
  const editor = page.locator('[contenteditable]');
  
  await editor.click();
  await editor.type('Hello world');
  
  await expect(editor).toHaveText('Hello world');
});

test('should handle IME composition', async ({ page }) => {
  await page.goto('/editor');
  const editor = page.locator('[contenteditable]');
  
  await editor.click();
  // Type Korean characters
  await editor.pressSequentially('한글');
  
  await expect(editor).toContainText('한글');
});

Mobile Testing

Mobile testing requires real devices or emulation.

test('should handle virtual keyboard', async ({ page, isMobile }) => {
  if (!isMobile) test.skip();
  
  await page.goto('/editor');
  const editor = page.locator('[contenteditable]');
  
  await editor.tap();
  // Virtual keyboard should appear
  await editor.type('Hello');
  
  await expect(editor).toHaveText('Hello');
});

Performance Testing

Test editor performance with large documents.

test('should handle large documents', async () => {
  const editor = new Editor();
  const largeText = 'Hello '.repeat(10000);
  
  const start = performance.now();
  editor.setContent(largeText);
  const end = performance.now();
  
  expect(end - start).toBeLessThan(100); // Should complete in < 100ms
});

Best Practices

  • Test model operations in isolation
  • Test DOM synchronization separately
  • Use real IMEs for composition testing
  • Test across multiple browsers
  • Test on real mobile devices
  • Mock external dependencies
  • Test edge cases and error conditions