CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-preact

Fast 3kb React-compatible Virtual DOM library.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

testing.mddocs/

Testing Utilities

Testing helpers for managing component rendering, effects flushing, and test environment setup. These utilities ensure reliable testing of Preact components with proper lifecycle management.

Capabilities

Test Environment Setup

Functions for initializing and managing the test environment.

/**
 * Sets up the test environment and returns a rerender function
 * @returns Function to trigger component re-renders in tests
 */
function setupRerender(): () => void;

/**
 * Wraps test code to ensure all effects and updates are flushed
 * @param callback - Test code to execute
 * @returns Promise that resolves when all updates are complete
 */
function act(callback: () => void | Promise<void>): Promise<void>;

/**
 * Resets the test environment and cleans up Preact state
 * Should be called after each test to ensure clean state
 */
function teardown(): void;

Usage Examples:

import { setupRerender, act, teardown, render, createElement } from "preact/test-utils";
import { useState } from "preact/hooks";

// Basic test setup
describe('Component Tests', () => {
  let rerender: () => void;
  
  beforeEach(() => {
    rerender = setupRerender();
  });
  
  afterEach(() => {
    teardown();
  });
  
  test('component renders correctly', async () => {
    function Counter() {
      const [count, setCount] = useState(0);
      
      return createElement("div", null,
        createElement("span", { "data-testid": "count" }, count),
        createElement("button", { 
          onClick: () => setCount(c => c + 1),
          "data-testid": "increment"
        }, "Increment")
      );
    }
    
    // Render component
    const container = document.createElement('div');
    document.body.appendChild(container);
    
    await act(() => {
      render(createElement(Counter), container);
    });
    
    const countElement = container.querySelector('[data-testid="count"]');
    const buttonElement = container.querySelector('[data-testid="increment"]');
    
    expect(countElement?.textContent).toBe('0');
    
    // Test interaction
    await act(() => {
      buttonElement?.click();
    });
    
    expect(countElement?.textContent).toBe('1');
    
    // Cleanup
    document.body.removeChild(container);
  });
});

// Testing with async effects
test('component with async effects', async () => {
  const rerender = setupRerender();
  
  function AsyncComponent({ userId }: { userId: number }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    
    useEffect(() => {
      const fetchUser = async () => {
        setLoading(true);
        // Mock async operation
        await new Promise(resolve => setTimeout(resolve, 100));
        setUser({ id: userId, name: `User ${userId}` });
        setLoading(false);
      };
      
      fetchUser();
    }, [userId]);
    
    if (loading) return createElement("div", null, "Loading...");
    if (!user) return createElement("div", null, "No user");
    
    return createElement("div", null, user.name);
  }
  
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  await act(async () => {
    render(createElement(AsyncComponent, { userId: 1 }), container);
  });
  
  // Initially loading
  expect(container.textContent).toBe('Loading...');
  
  // Wait for async effect to complete
  await act(async () => {
    await new Promise(resolve => setTimeout(resolve, 150));
  });
  
  expect(container.textContent).toBe('User 1');
  
  document.body.removeChild(container);
  teardown();
});

Component Testing Patterns

Common patterns for testing Preact components effectively.

Usage Examples:

import { setupRerender, act, teardown, render, createElement } from "preact/test-utils";
import { useState, useEffect } from "preact/hooks";

// Testing component state changes
function testComponentState() {
  const rerender = setupRerender();
  
  function StatefulComponent() {
    const [state, setState] = useState({ count: 0, name: '' });
    
    const updateCount = () => setState(prev => ({ ...prev, count: prev.count + 1 }));
    const updateName = (name: string) => setState(prev => ({ ...prev, name }));
    
    return createElement("div", null,
      createElement("div", { "data-testid": "count" }, state.count),
      createElement("div", { "data-testid": "name" }, state.name),
      createElement("button", { 
        onClick: updateCount,
        "data-testid": "count-btn"
      }, "Count++"),
      createElement("input", {
        value: state.name,
        onChange: (e) => updateName((e.target as HTMLInputElement).value),
        "data-testid": "name-input"
      })
    );
  }
  
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  act(() => {
    render(createElement(StatefulComponent), container);
  });
  
  const countEl = container.querySelector('[data-testid="count"]') as HTMLElement;
  const nameEl = container.querySelector('[data-testid="name"]') as HTMLElement;
  const countBtn = container.querySelector('[data-testid="count-btn"]') as HTMLButtonElement;
  const nameInput = container.querySelector('[data-testid="name-input"]') as HTMLInputElement;
  
  // Initial state
  expect(countEl.textContent).toBe('0');
  expect(nameEl.textContent).toBe('');
  
  // Test count update
  act(() => {
    countBtn.click();
  });
  
  expect(countEl.textContent).toBe('1');
  
  // Test name update
  act(() => {
    nameInput.value = 'John';
    nameInput.dispatchEvent(new Event('input', { bubbles: true }));
  });
  
  expect(nameEl.textContent).toBe('John');
  
  document.body.removeChild(container);
  teardown();
}

// Testing component with context
function testComponentWithContext() {
  const rerender = setupRerender();
  
  const TestContext = createContext({ value: 'default' });
  
  function ContextConsumer() {
    const { value } = useContext(TestContext);
    return createElement("div", { "data-testid": "context-value" }, value);
  }
  
  function TestWrapper({ contextValue, children }: { 
    contextValue: string; 
    children: ComponentChildren;
  }) {
    return createElement(TestContext.Provider, { 
      value: { value: contextValue } 
    }, children);
  }
  
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  act(() => {
    render(
      createElement(TestWrapper, { contextValue: 'test-value' },
        createElement(ContextConsumer)
      ),
      container
    );
  });
  
  const valueEl = container.querySelector('[data-testid="context-value"]') as HTMLElement;
  expect(valueEl.textContent).toBe('test-value');
  
  document.body.removeChild(container);
  teardown();
}

// Testing component lifecycle
function testComponentLifecycle() {
  const rerender = setupRerender();
  const mountSpy = jest.fn();
  const unmountSpy = jest.fn();
  const updateSpy = jest.fn();
  
  function LifecycleComponent({ value }: { value: string }) {
    useEffect(() => {
      mountSpy();
      return () => unmountSpy();
    }, []);
    
    useEffect(() => {
      updateSpy(value);
    }, [value]);
    
    return createElement("div", null, value);
  }
  
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  // Mount
  act(() => {
    render(createElement(LifecycleComponent, { value: 'initial' }), container);
  });
  
  expect(mountSpy).toHaveBeenCalledTimes(1);
  expect(updateSpy).toHaveBeenCalledWith('initial');
  
  // Update
  act(() => {
    render(createElement(LifecycleComponent, { value: 'updated' }), container);
  });
  
  expect(updateSpy).toHaveBeenCalledWith('updated');
  
  // Unmount
  act(() => {
    render(null, container);
  });
  
  expect(unmountSpy).toHaveBeenCalledTimes(1);
  
  document.body.removeChild(container);
  teardown();
}

Advanced Testing Utilities

Helper functions and patterns for complex testing scenarios.

Usage Examples:

import { setupRerender, act, teardown } from "preact/test-utils";

// Custom testing utilities
class TestRenderer {
  container: HTMLElement;
  rerender: () => void;
  
  constructor() {
    this.container = document.createElement('div');
    document.body.appendChild(this.container);
    this.rerender = setupRerender();
  }
  
  render(element: ComponentChildren) {
    return act(() => {
      render(element, this.container);
    });
  }
  
  update(element: ComponentChildren) {
    return this.render(element);
  }
  
  findByTestId(testId: string): HTMLElement | null {
    return this.container.querySelector(`[data-testid="${testId}"]`);
  }
  
  findAllByTestId(testId: string): NodeListOf<HTMLElement> {
    return this.container.querySelectorAll(`[data-testid="${testId}"]`);
  }
  
  findByText(text: string): HTMLElement | null {
    const walker = document.createTreeWalker(
      this.container,
      NodeFilter.SHOW_TEXT,
      null,
      false
    );
    
    let node;
    while (node = walker.nextNode()) {
      if (node.textContent?.includes(text)) {
        return node.parentElement;
      }
    }
    return null;
  }
  
  cleanup() {
    document.body.removeChild(this.container);
    teardown();
  }
}

// Mock async operations
function mockAsyncOperation<T>(result: T, delay = 100): Promise<T> {
  return new Promise(resolve => {
    setTimeout(() => resolve(result), delay);
  });
}

// Testing hook-based components
function TestHookComponent({ hook }: { hook: () => any }) {
  const result = hook();
  
  return createElement("div", {
    "data-testid": "hook-result"
  }, JSON.stringify(result));
}

function testCustomHook(hook: () => any) {
  const renderer = new TestRenderer();
  
  renderer.render(createElement(TestHookComponent, { hook }));
  
  const getResult = () => {
    const element = renderer.findByTestId('hook-result');
    return element ? JSON.parse(element.textContent || '{}') : null;
  };
  
  return {
    result: { current: getResult() },
    rerender: (newHook?: () => any) => {
      renderer.update(createElement(TestHookComponent, { hook: newHook || hook }));
    },
    cleanup: () => renderer.cleanup()
  };
}

// Example: Testing custom hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}

test('useCounter hook', () => {
  const { result, rerender, cleanup } = testCustomHook(() => useCounter(5));
  
  expect(result.current.count).toBe(5);
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(6);
  
  act(() => {
    result.current.decrement();
  });
  
  expect(result.current.count).toBe(5);
  
  act(() => {
    result.current.reset();
  });
  
  expect(result.current.count).toBe(5);
  
  cleanup();
});

// Testing error boundaries
function TestErrorBoundary({ 
  children, 
  onError 
}: { 
  children: ComponentChildren;
  onError?: (error: Error) => void;
}) {
  return createElement(ErrorBoundary, { 
    onError,
    fallback: createElement("div", { "data-testid": "error" }, "Error occurred")
  }, children);
}

function ErrorThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) {
    throw new Error('Test error');
  }
  
  return createElement("div", { "data-testid": "success" }, "No error");
}

test('error boundary handling', () => {
  const renderer = new TestRenderer();
  const errorSpy = jest.fn();
  
  // Render without error
  renderer.render(
    createElement(TestErrorBoundary, { onError: errorSpy },
      createElement(ErrorThrowingComponent, { shouldThrow: false })
    )
  );
  
  expect(renderer.findByTestId('success')).toBeTruthy();
  expect(renderer.findByTestId('error')).toBeFalsy();
  
  // Render with error
  act(() => {
    renderer.update(
      createElement(TestErrorBoundary, { onError: errorSpy },
        createElement(ErrorThrowingComponent, { shouldThrow: true })
      )
    );
  });
  
  expect(renderer.findByTestId('error')).toBeTruthy();
  expect(renderer.findByTestId('success')).toBeFalsy();
  expect(errorSpy).toHaveBeenCalledWith(expect.any(Error));
  
  renderer.cleanup();
});

Integration Testing Patterns

Patterns for testing component integration and complex interactions.

Usage Examples:

// Testing form components
function testFormIntegration() {
  const renderer = new TestRenderer();
  
  function ContactForm() {
    const [formData, setFormData] = useState({
      name: '',
      email: '',
      message: ''
    });
    const [submitted, setSubmitted] = useState(false);
    
    const handleSubmit = (e: Event) => {
      e.preventDefault();
      setSubmitted(true);
    };
    
    const updateField = (field: string, value: string) => {
      setFormData(prev => ({ ...prev, [field]: value }));
    };
    
    if (submitted) {
      return createElement("div", { "data-testid": "success" }, "Form submitted!");
    }
    
    return createElement("form", { onSubmit: handleSubmit },
      createElement("input", {
        "data-testid": "name",
        value: formData.name,
        onChange: (e) => updateField('name', (e.target as HTMLInputElement).value),
        placeholder: "Name"
      }),
      createElement("input", {
        "data-testid": "email",
        value: formData.email,
        onChange: (e) => updateField('email', (e.target as HTMLInputElement).value),
        placeholder: "Email"
      }),
      createElement("textarea", {
        "data-testid": "message",
        value: formData.message,
        onChange: (e) => updateField('message', (e.target as HTMLTextAreaElement).value),
        placeholder: "Message"
      }),
      createElement("button", { 
        type: "submit",
        "data-testid": "submit"
      }, "Submit")
    );
  }
  
  act(() => {
    renderer.render(createElement(ContactForm));
  });
  
  const nameInput = renderer.findByTestId('name') as HTMLInputElement;
  const emailInput = renderer.findByTestId('email') as HTMLInputElement;
  const messageInput = renderer.findByTestId('message') as HTMLTextAreaElement;
  const submitButton = renderer.findByTestId('submit') as HTMLButtonElement;
  
  // Fill form
  act(() => {
    nameInput.value = 'John Doe';
    nameInput.dispatchEvent(new Event('input', { bubbles: true }));
    
    emailInput.value = 'john@example.com';
    emailInput.dispatchEvent(new Event('input', { bubbles: true }));
    
    messageInput.value = 'Hello world';
    messageInput.dispatchEvent(new Event('input', { bubbles: true }));
  });
  
  // Submit form
  act(() => {
    submitButton.click();
  });
  
  expect(renderer.findByTestId('success')).toBeTruthy();
  
  renderer.cleanup();
}

Install with Tessl CLI

npx tessl i tessl/npm-preact

docs

compat.md

components.md

context.md

core.md

devtools.md

hooks.md

index.md

jsx-runtime.md

testing.md

tile.json