React Component Testing Framework
This challenge asks you to build a foundational component testing framework for React applications using TypeScript. A robust testing framework is crucial for ensuring the reliability and maintainability of your React components, allowing you to confidently refactor and add new features. You will create a system that can render components, query their elements, and assert on their properties and behavior.
Problem Description
Your task is to implement a simplified testing framework that allows users to test React components. This framework should provide utilities to:
- Render a component: Given a React component and its props, the framework should be able to render it into a virtual DOM or a similar representation.
- Query elements: After rendering, the framework should allow users to select specific DOM elements within the rendered component using selectors (e.g., CSS selectors).
- Assert on elements: Users should be able to make assertions about the selected elements, such as checking their text content, attributes, or whether they exist.
Key Requirements:
- TypeScript Support: All functions and types should be defined in TypeScript.
renderfunction: This function will take a React element (created with JSX) and return an object with methods for querying and asserting.screenobject: This will be a global or imported object that provides access to querying functions.- Querying methods: Implement at least
getByText,getByRole, andgetByLabelText. These methods should accept a selector (string or specific types) and return the matched DOM element. If no element is found, they should throw a descriptive error. - Assertion methods: The object returned by
rendershould expose assertion methods liketoBeInTheDocument(checks for existence),toHaveTextContent, andtoHaveAttribute. - No external testing libraries: For this challenge, assume you are building these core utilities from scratch, without relying on popular libraries like
@testing-library/reactor Jest's DOM matchers directly. However, you can use React's test utilities (likecreateRootfromreact-dom/client) if necessary for rendering. - Descriptive errors: When queries or assertions fail, the error messages should be clear and helpful for debugging.
Expected Behavior:
A typical test case would look something like this:
// Inside a test file
import { render, screen } from './testing-library'; // Assume this is your framework
const MyComponent = ({ name }: { name: string }) => (
<div>
<h1>Welcome, {name}!</h1>
<button aria-label="Submit">Submit</button>
</div>
);
test('renders component correctly', () => {
const { getByRole } = render(<MyComponent name="Alice" />);
const heading = screen.getByRole('heading', { name: 'Welcome, Alice!' });
expect(heading).toBeInTheDocument();
const button = getByRole('button', { name: 'Submit' });
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('aria-label', 'Submit');
});
Edge Cases:
- Multiple elements matching a query: The
getBymethods should throw an error if more than one element matches. - No element matching a query: The
getBymethods should throw an error if no element matches. - Complex DOM structures: Ensure queries can navigate through nested elements.
Examples
Example 1:
Input Component:
const Greeting = ({ message }: { message: string }) => {
return <p>{message}</p>;
};
Test Case:
import { render, screen } from './testing-library';
test('renders a greeting message', () => {
const { getByText } = render(<Greeting message="Hello, World!" />);
const messageElement = screen.getByText('Hello, World!');
expect(messageElement).toBeInTheDocument();
});
Output (Conceptual): The render function would internally create a React root, render Greeting into it, and return an object. screen.getByText('Hello, World!') would find the <p> element. expect(messageElement).toBeInTheDocument() would pass because the element was found.
Example 2:
Input Component:
const InputField = ({ label, placeholder }: { label: string; placeholder: string }) => (
<div>
<label htmlFor="input-field">{label}</label>
<input id="input-field" placeholder={placeholder} />
</div>
);
Test Case:
import { render, screen } from './testing-library';
test('renders input with correct label and placeholder', () => {
const { getByLabelText } = render(
<InputField label="Username" placeholder="Enter your name" />
);
const input = screen.getByLabelText('Username');
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute('placeholder', 'Enter your name');
});
Output (Conceptual): render renders InputField. screen.getByLabelText('Username') finds the <input> element associated with the <label>. expect(input).toHaveAttribute('placeholder', 'Enter your name') verifies the placeholder text.
Example 3: Error Handling
Input Component:
const MyButton = ({ onClick, children }: { onClick: () => void; children: React.ReactNode }) => (
<button onClick={onClick}>{children}</button>
);
Test Case (Failing Query):
import { render, screen } from './testing-library';
test('throws error if element not found', () => {
const { getByText } = render(<MyButton onClick={() => {}}>Click Me</MyButton>);
// This query will fail because "Non-existent" is not in the component
expect(() => screen.getByText('Non-existent')).toThrow('Unable to find an element with the text "Non-existent"');
});
test('throws error if element is not unique', () => {
const { getAllByRole } = render(
<>
<button role="button">Button 1</button>
<button role="button">Button 2</button>
</>
);
// getByRole should throw an error when multiple buttons are found
expect(() => screen.getByRole('button', { name: '' })).toThrow('Found multiple elements with the role "button"');
});
Output (Conceptual): The first test would catch an error thrown by screen.getByText stating it couldn't find the text. The second test would catch an error from screen.getByRole indicating multiple elements with the role "button" were found.
Constraints
- DOM Manipulation: You should use React's testing utilities for rendering into a document fragment or virtual DOM. Avoid direct manipulation of the actual browser
document. - TypeScript: Strict TypeScript configuration is expected for all code.
- No External Testing Libraries: Do not use
@testing-library/react, Jest's built-in DOM matchers (.toBeInTheDocument, etc.), or Enzyme. You are building the core logic for these. You may usejest-dommatchers if you are defining your own assertions that mimic their functionality. - Performance: While not a primary focus for this foundational challenge, avoid excessively inefficient DOM traversals.
Notes
- Consider how you will represent the rendered component and its DOM structure. You might explore using
React.createElementand a simple DOM-like structure. - Think about how to map user-friendly selectors (like text content) to actual DOM node properties.
- The
screenobject is a common pattern to avoid passing therenderresult around explicitly for every query. - For assertions, you'll need to define your own matcher functions or return an object with methods that perform the checks.
- This challenge is about understanding the principles behind component testing frameworks, not about replicating the full functionality of existing libraries. Focus on the core rendering, querying, and basic assertion mechanisms.