Back to Repositories

Testing Context Propagation and State Management in PreactJS

This test suite examines the context propagation behavior in Preact’s compatibility layer, focusing on nested context updates and state management. It verifies synchronous updates across component trees while testing the interaction between React-like context API and local state management.

Test Coverage Overview

The test suite provides comprehensive coverage of context propagation in Preact’s React compatibility layer. It tests:

  • Nested context updates through Provider/Consumer pattern
  • Synchronous state propagation across component trees
  • Integration between context updates and local state changes
  • Router-like context implementation scenarios

Implementation Analysis

The testing approach implements a mock router system using React’s context API to validate state propagation. It utilizes both class and functional components, demonstrating the interplay between useState and useContext hooks, while validating component re-rendering behavior through state changes.

  • Component hierarchy testing with Provider/Consumer pattern
  • Hook implementation verification
  • State synchronization validation

Technical Details

Testing infrastructure includes:

  • Jest test framework integration
  • Preact test-utils for rerender functionality
  • Custom scratch element setup and teardown
  • Mock Router implementation for context testing
  • State tracking through pageRenders array

Best Practices Demonstrated

The test suite exemplifies several testing best practices for context-based applications. It maintains clear separation of concerns between routing, context management, and component rendering while implementing proper setup and teardown procedures. The test structure ensures isolated testing environments and comprehensive state tracking.

  • Proper test setup and cleanup
  • Isolated component testing
  • Comprehensive state verification
  • Clear test case organization

preactjs/preact

compat/test/browser/context.test.js

            
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import React, {
	render,
	createElement,
	createContext,
	Component,
	useState,
	useContext
} from 'preact/compat';

describe('components', () => {
	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it('nested context updates propagate throughout the tree synchronously', () => {
		const RouterContext = createContext({ location: '__default_value__' });

		const route1 = '/page/1';
		const route2 = '/page/2';

		/** @type {() => void} */
		let toggleLocalState;
		/** @type {() => void} */
		let toggleLocation;

		/** @type {Array<{location: string, localState: boolean}>} */
		let pageRenders = [];

		function runUpdate() {
			toggleLocalState();
			toggleLocation();
		}

		/**
		 * @extends {React.Component<{children: any}, {location: string}>}
		 */
		class Router extends Component {
			constructor(props) {
				super(props);
				this.state = { location: route1 };
				toggleLocation = () => {
					const oldLocation = this.state.location;
					const newLocation = oldLocation === route1 ? route2 : route1;
					// console.log('Toggling  location', oldLocation, '->', newLocation);
					this.setState({ location: newLocation });
				};
			}

			render() {
				// console.log('Rendering Router', { location: this.state.location });
				return (
					<RouterContext.Provider value={{ location: this.state.location }}>
						{this.props.children}
					</RouterContext.Provider>
				);
			}
		}

		/**
		 * @extends {React.Component<{children: any}>}
		 */
		class Route extends Component {
			render() {
				return (
					<RouterContext.Consumer>
						{contextValue => {
							// console.log('Rendering Route', {
							// 	location: contextValue.location
							// });
							// Pretend to do something with the context value
							const newContextValue = { ...contextValue };
							return (
								<RouterContext.Provider value={newContextValue}>
									{this.props.children}
								</RouterContext.Provider>
							);
						}}
					</RouterContext.Consumer>
				);
			}
		}

		function Page() {
			const [localState, setLocalState] = useState(true);
			const { location } = useContext(RouterContext);

			pageRenders.push({ location, localState });
			// console.log('Rendering Page', { location, localState });

			toggleLocalState = () => {
				let newValue = !localState;
				// console.log('Toggling  localState', localState, '->', newValue);
				setLocalState(newValue);
			};

			return (
				<>
					<div>localState: {localState.toString()}</div>
					<div>location: {location}</div>
					<div>
						<button type="button" onClick={runUpdate}>
							Trigger update
						</button>
					</div>
				</>
			);
		}

		function App() {
			return (
				<Router>
					<Route>
						<Page />
					</Route>
				</Router>
			);
		}

		render(<App />, scratch);
		expect(pageRenders).to.deep.equal([{ location: route1, localState: true }]);

		pageRenders = [];
		runUpdate(); // Simulate button click
		rerender();

		// Page should rerender once with both propagated context and local state updates
		expect(pageRenders).to.deep.equal([
			{ location: route2, localState: false }
		]);
	});
});