npm.io
2.0.3 • Published 2 weeks ago

@pie-lib/test-utils

Licence
ISC
Version
2.0.3
Deps
4
Size
106 kB
Vulns
0
Weekly
0
Stars
4

@pie-lib/test-utils

Testing utilities for pie-lib packages with React Testing Library and MUI support.

Installation

This package is typically used as a dev dependency:

yarn add -D @pie-lib/test-utils

Features

  • React Testing Library utilities
  • MUI Theme Provider wrapper for tests
  • Multiple provider composition
  • Custom theme creation for testing
  • All RTL exports in one place
  • userEvent and jest-dom matchers included
  • Keyboard helpers for keyCode-based components
  • Web component testing utilities (light DOM only)

Usage

Basic Rendering with MUI Theme
import { renderWithTheme, screen } from '@pie-lib/test-utils';
import { Button } from '@mui/material';

test('renders a button', () => {
  renderWithTheme(<Button>Click me</Button>);
  expect(screen.getByRole('button')).toBeInTheDocument();
});
Custom Theme
import { renderWithTheme, createTestTheme } from '@pie-lib/test-utils';

test('renders with dark theme', () => {
  const darkTheme = createTestTheme({
    palette: { mode: 'dark' }
  });

  renderWithTheme(<MyComponent />, { theme: darkTheme });
  // ... assertions
});
Multiple Providers
import { renderWithProviders } from '@pie-lib/test-utils';

test('renders with multiple providers', () => {
  const ReduxProvider = ({ children }) => (
    <Provider store={store}>{children}</Provider>
  );

  renderWithProviders(<MyComponent />, {
    providers: [ReduxProvider]
  });
  // ... assertions
});
User Interactions
import { renderWithTheme, screen, userEvent } from '@pie-lib/test-utils';

test('handles button click', async () => {
  const onClick = jest.fn();
  const user = userEvent.setup();

  renderWithTheme(<Button onClick={onClick}>Click</Button>);

  await user.click(screen.getByRole('button'));
  expect(onClick).toHaveBeenCalledTimes(1);
});
Keyboard Interactions

NEW: Helper functions for testing keyboard events, especially useful for legacy components checking event.keyCode:

import { render, screen, pressKey, Keys, typeAndSubmit } from '@pie-lib/test-utils';

test('submits on Enter key', async () => {
  const onSubmit = jest.fn();
  render(<SearchInput onSubmit={onSubmit} />);

  const input = screen.getByRole('textbox');
  await typeAndSubmit(input, 'search term');

  expect(onSubmit).toHaveBeenCalledWith('search term');
});

test('handles Escape key', () => {
  const onCancel = jest.fn();
  render(<Modal onCancel={onCancel} />);

  const modal = screen.getByRole('dialog');
  pressKey(modal, Keys.ESCAPE);

  expect(onCancel).toHaveBeenCalled();
});
Async Operations
import { renderWithTheme, screen, waitFor } from '@pie-lib/test-utils';

test('loads data', async () => {
  renderWithTheme(<AsyncComponent />);

  // Wait for element to appear
  const element = await screen.findByText('Loaded');
  expect(element).toBeInTheDocument();

  // Or use waitFor
  await waitFor(() => {
    expect(screen.getByText('Loaded')).toBeInTheDocument();
  });
});
Web Component Testing

Note: Utilities for testing custom elements with light DOM (no Shadow DOM). Standard React Testing Library queries work directly on these components.

import { renderWebComponent, dispatchCustomEvent, screen } from '@pie-lib/test-utils';

test('interacts with web component', async () => {
  const element = await renderWebComponent('my-custom-element', {
    'data-testid': 'my-element',
    value: '42'
  });

  // Query using standard RTL queries (light DOM)
  const button = screen.getByRole('button');
  await user.click(button);

  // Dispatch custom events
  dispatchCustomEvent(element, 'value-changed', { newValue: '100' });
});

API Reference

React Component Rendering
renderWithTheme(ui, options)

Renders a component wrapped with MUI ThemeProvider.

Parameters:

  • ui - React component to render
  • options.theme - Custom MUI theme (optional, defaults to Material-UI default)
  • options... - Additional options passed to RTL's render()

Returns: RTL render result

renderWithProviders(ui, options)

Renders a component with MUI ThemeProvider and additional custom providers.

Parameters:

  • ui - React component to render
  • options.theme - Custom MUI theme (optional)
  • options.providers - Array of provider components to wrap (optional)
  • options... - Additional options passed to RTL's render()

Returns: RTL render result

createTestTheme(themeOptions)

Creates a MUI theme for testing.

Parameters:

  • themeOptions - MUI theme configuration object

Returns: MUI theme object

waitForRemoval(callback)

Alias for waitForElementToBeRemoved from RTL.

Re-exports

This package re-exports everything from:

  • @testing-library/react (render, screen, waitFor, etc.)
  • @testing-library/user-event (as userEvent)
  • @testing-library/jest-dom (for TypeScript support)

Keyboard Helpers API

Keys

Constants for common keyboard key codes:

import { Keys } from '@pie-lib/test-utils';

Keys.ENTER      // 13
Keys.ESCAPE     // 27
Keys.SPACE      // 32
Keys.TAB        // 9
Keys.ARROW_DOWN // 40
Keys.ARROW_UP   // 38
// ... and more
pressKey(element, keyCode, type, options)

Simulate keyboard event with keyCode (useful for legacy components):

import { pressKey, Keys } from '@pie-lib/test-utils';

// Press Enter
pressKey(input, Keys.ENTER);

// Press Escape with keyup event
pressKey(dialog, Keys.ESCAPE, 'keyup');

// Press Ctrl+Enter
pressKey(input, Keys.ENTER, 'keydown', { ctrlKey: true });

Why? userEvent.type() with {Enter} doesn't work well with components checking event.keyCode.

typeAndSubmit(element, text)

Type text and press Enter (common pattern):

import { typeAndSubmit } from '@pie-lib/test-utils';

await typeAndSubmit(input, 'search query');
// Types "search query" and presses Enter
clearAndType(element, text)

Clear input and type new text:

import { clearAndType } from '@pie-lib/test-utils';

await clearAndType(input, 'new value');
navigateWithKeys(element, steps, direction)

Navigate with arrow keys:

import { navigateWithKeys } from '@pie-lib/test-utils';

// Navigate down 3 items
navigateWithKeys(listbox, 3, 'vertical');

// Navigate left 2 items
navigateWithKeys(tabs, -2, 'horizontal');

Web Component Helpers API

Note: These utilities are for light DOM web components only (no Shadow DOM). For Shadow DOM components, use standard DOM queries or other testing libraries.

waitForCustomElement(tagName, timeout)

Wait for custom element to be defined:

import { waitForCustomElement } from '@pie-lib/test-utils';

await waitForCustomElement('my-component');
await waitForCustomElement('pie-chart', 5000); // 5 second timeout
renderWebComponent(tagName, attributes, properties, container)

Render web component and wait for it to be ready:

import { renderWebComponent } from '@pie-lib/test-utils';

const element = await renderWebComponent('pie-chart',
  { type: 'bar', 'data-testid': 'chart' },  // attributes
  { onDataChange: jest.fn() }               // properties
);
dispatchCustomEvent(element, eventName, detail, options)

Dispatch custom event on element:

import { dispatchCustomEvent } from '@pie-lib/test-utils';

dispatchCustomEvent(chart, 'data-changed', { value: [1, 2, 3] });
waitForEvent(element, eventName, timeout)

Wait for custom event to be fired:

import { waitForEvent } from '@pie-lib/test-utils';

const promise = waitForEvent(component, 'ready');
component.initialize();
const event = await promise;
expect(event.detail).toEqual({ initialized: true });
isCustomElementDefined(tagName)

Check if a custom element is defined:

import { isCustomElementDefined } from '@pie-lib/test-utils';

if (isCustomElementDefined('pie-chart')) {
  // Element is ready to use
}
createCustomElement(tagName, props)

Create and configure a custom element:

import { createCustomElement } from '@pie-lib/test-utils';

const chart = createCustomElement('pie-chart', {
  data: [1, 2, 3],
  type: 'bar'
});
document.body.appendChild(chart);

Migration from Enzyme

Before (Enzyme)
import { shallow } from 'enzyme';

const wrapper = shallow(<Component />);
expect(wrapper.find('button')).toHaveLength(1);
wrapper.find('button').simulate('click');
After (React Testing Library)
import { renderWithTheme, screen, userEvent } from '@pie-lib/test-utils';

renderWithTheme(<Component />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
await userEvent.click(button);

Query Priority

Use queries in this order (best to worst):

  1. getByRole - Best for accessibility
  2. getByLabelText - Good for form fields
  3. getByPlaceholderText - Forms when label not available
  4. getByText - Non-interactive content
  5. getByDisplayValue - Current form values
  6. getByAltText - Images
  7. getByTitle - When other options don't work
  8. getByTestId - Last resort

Common Patterns

Testing Forms
import { renderWithTheme, screen, userEvent } from '@pie-lib/test-utils';

test('submits form', async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();

  renderWithTheme(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText('Username'), 'user123');
  await user.type(screen.getByLabelText('Password'), 'pass123');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    username: 'user123',
    password: 'pass123'
  });
});
Testing Conditional Rendering
test('shows error message', async () => {
  renderWithTheme(<Component />);

  // Initially not present
  expect(screen.queryByText('Error')).not.toBeInTheDocument();

  // Trigger error
  await userEvent.click(screen.getByRole('button'));

  // Now appears
  expect(await screen.findByText('Error occurred')).toBeInTheDocument();
});
Testing with Custom Props
const defaultProps = {
  title: 'Test',
  onChange: jest.fn()
};

test('renders with defaults', () => {
  renderWithTheme(<Component {...defaultProps} />);
  expect(screen.getByText('Test')).toBeInTheDocument();
});

test('renders with overrides', () => {
  renderWithTheme(<Component {...defaultProps} title="Override" />);
  expect(screen.getByText('Override')).toBeInTheDocument();
});

Tips

  1. Use screen queries - More readable than destructuring from render
  2. Prefer findBy for async - Built-in waiting
  3. Use queryBy for non-existence - Won't throw if not found
  4. Test behavior, not implementation - Don't test internal state
  5. Use userEvent over fireEvent - More realistic interactions

Learn More