Back to Repositories

Testing Context Provider-Consumer Integration in Preact

This test suite validates the createContext functionality in Preact, focusing on context creation, provider-consumer interactions, and context propagation through component hierarchies. The tests ensure proper context value passing, nesting behavior, and performance optimizations.

Test Coverage Overview

The test suite provides comprehensive coverage of Preact’s context API implementation.

  • Context value propagation through providers and consumers
  • Nested context providers and value inheritance
  • Context updates and re-rendering optimization
  • Integration with class components via contextType
  • Edge cases like shouldComponentUpdate interactions

Implementation Analysis

Tests utilize Jest and Sinon for mocking and assertions, employing a component-based testing approach. The implementation verifies both functional and class component context usage, with particular attention to performance optimization patterns and proper cleanup.

  • Provider-Consumer relationship testing
  • Context value change propagation
  • Component lifecycle integration
  • Render optimization verification

Technical Details

  • Testing Framework: Jest
  • Assertion Library: Sinon
  • Test Utilities: preact/test-utils
  • Setup Helpers: setupScratch, teardown
  • Component Types: Class and Functional

Best Practices Demonstrated

The test suite showcases high-quality testing practices for context-based applications.

  • Isolated component testing
  • Proper setup and teardown
  • Comprehensive edge case coverage
  • Performance consideration testing
  • Clear test organization and structure

preactjs/preact

test/browser/createContext.test.js

            
import { setupRerender, act } from 'preact/test-utils';
import {
	createElement,
	render,
	Component,
	createContext,
	Fragment
} from 'preact';
import { setupScratch, teardown } from '../_util/helpers';

/** @jsx createElement */

describe('createContext', () => {
	let scratch;
	let rerender;

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

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

	it('should pass context to a consumer', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };

		let receivedContext;

		class Inner extends Component {
			render(props) {
				return <div>{props.a}</div>;
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<Provider value={CONTEXT}>
				<div>
					<Consumer>
						{data => {
							receivedContext = data;
							return <Inner {...data} />;
						}}
					</Consumer>
				</div>
			</Provider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWithMatch(CONTEXT);
		expect(receivedContext).to.equal(CONTEXT);
		expect(scratch.innerHTML).to.equal('<div><div>a</div></div>');
	});

	// This optimization helps
	// to prevent a Provider from rerendering the children, this means
	// we only propagate to children.
	// Strict equal vnode optimization
	it('skips referentially equal children to Provider', () => {
		const { Provider, Consumer } = createContext(null);
		let set,
			renders = 0;
		const Layout = ({ children }) => {
			renders++;
			return children;
		};
		class State extends Component {
			constructor(props) {
				super(props);
				this.state = { i: 0 };
				set = this.setState.bind(this);
			}
			render() {
				const { children } = this.props;
				return <Provider value={this.state}>{children}</Provider>;
			}
		}
		const App = () => (
			<State>
				<Layout>
					<Consumer>{({ i }) => <p>{i}</p>}</Consumer>
				</Layout>
			</State>
		);
		render(<App />, scratch);
		expect(renders).to.equal(1);
		set({ i: 2 });
		rerender();
		expect(renders).to.equal(1);
	});

	it('should preserve provider context through nesting providers', done => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };
		const CHILD_CONTEXT = { b: 'b' };

		let parentContext, childContext;

		class Inner extends Component {
			render(props) {
				return (
					<div>
						{props.a} - {props.b}
					</div>
				);
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<Provider value={CONTEXT}>
				<Consumer>
					{data => {
						parentContext = data;
						return (
							<Provider value={CHILD_CONTEXT}>
								<Consumer>
									{childData => {
										childContext = childData;
										return <Inner {...data} {...childData} />;
									}}
								</Consumer>
							</Provider>
						);
					}}
				</Consumer>
			</Provider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWithMatch({
			...CONTEXT,
			...CHILD_CONTEXT
		});
		expect(Inner.prototype.render).to.be.calledOnce;
		expect(parentContext).to.equal(CONTEXT);
		expect(childContext).to.equal(CHILD_CONTEXT);
		expect(scratch.innerHTML).to.equal('<div>a - b</div>');
		setTimeout(() => {
			expect(Inner.prototype.render).to.be.calledOnce;
			done();
		}, 0);
	});

	it('should preserve provider context between different providers', () => {
		const { Provider: ThemeProvider, Consumer: ThemeConsumer } =
			createContext(null);
		const { Provider: DataProvider, Consumer: DataConsumer } = createContext(null);
		const THEME_CONTEXT = { theme: 'black' };
		const DATA_CONTEXT = { global: 'a' };

		let receivedTheme;
		let receivedData;

		class Inner extends Component {
			render(props) {
				return (
					<div>
						{props.theme} - {props.global}
					</div>
				);
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<ThemeProvider value={THEME_CONTEXT.theme}>
				<DataProvider value={DATA_CONTEXT}>
					<ThemeConsumer>
						{theme => {
							receivedTheme = theme;
							return (
								<DataConsumer>
									{data => {
										receivedData = data;
										return <Inner theme={theme} {...data} />;
									}}
								</DataConsumer>
							);
						}}
					</ThemeConsumer>
				</DataProvider>
			</ThemeProvider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWithMatch({
			...THEME_CONTEXT,
			...DATA_CONTEXT
		});
		expect(receivedTheme).to.equal(THEME_CONTEXT.theme);
		expect(receivedData).to.equal(DATA_CONTEXT);
		expect(scratch.innerHTML).to.equal('<div>black - a</div>');
	});

	it('should preserve provider context through nesting consumers', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };

		let receivedData;
		let receivedChildData;

		class Inner extends Component {
			render(props) {
				return <div>{props.a}</div>;
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<Provider value={CONTEXT}>
				<Consumer>
					{data => {
						receivedData = data;
						return (
							<Consumer>
								{childData => {
									receivedChildData = childData;
									return <Inner {...data} {...childData} />;
								}}
							</Consumer>
						);
					}}
				</Consumer>
			</Provider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWithMatch({ ...CONTEXT });
		expect(receivedData).to.equal(CONTEXT);
		expect(receivedChildData).to.equal(CONTEXT);
		expect(scratch.innerHTML).to.equal('<div>a</div>');
	});

	it('should not emit when value does not update', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };

		class NoUpdate extends Component {
			shouldComponentUpdate() {
				return false;
			}

			render() {
				return this.props.children;
			}
		}

		class Inner extends Component {
			render(props) {
				return <div>{props.a}</div>;
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<div>
				<Provider value={CONTEXT}>
					<NoUpdate>
						<Consumer>{data => <Inner {...data} />}</Consumer>
					</NoUpdate>
				</Provider>
			</div>,
			scratch
		);

		expect(Inner.prototype.render).to.have.been.calledOnce;

		render(
			<div>
				<Provider value={CONTEXT}>
					<NoUpdate>
						<Consumer>{data => <Inner {...data} />}</Consumer>
					</NoUpdate>
				</Provider>
			</div>,
			scratch
		);

		expect(Inner.prototype.render).to.have.been.calledOnce;
	});

	it('should preserve provider context through nested components', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };

		let receivedContext;

		class Consumed extends Component {
			render(props) {
				return <strong>{props.a}</strong>;
			}
		}

		sinon.spy(Consumed.prototype, 'render');

		class Outer extends Component {
			render() {
				return (
					<div>
						<Inner />
					</div>
				);
			}
		}

		class Inner extends Component {
			render() {
				return (
					<Fragment>
						<InnerMost />
					</Fragment>
				);
			}
		}

		class InnerMost extends Component {
			render() {
				return (
					<div>
						<Consumer>
							{data => {
								receivedContext = data;
								return <Consumed {...data} />;
							}}
						</Consumer>
					</div>
				);
			}
		}

		render(
			<Provider value={CONTEXT}>
				<Outer />
			</Provider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Consumed.prototype.render).to.have.been.calledWithMatch({
			...CONTEXT
		});
		expect(receivedContext).to.equal(CONTEXT);
		expect(scratch.innerHTML).to.equal(
			'<div><div><strong>a</strong></div></div>'
		);
	});

	it('should propagates through shouldComponentUpdate false', done => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { a: 'a' };
		const UPDATED_CONTEXT = { a: 'b' };

		class Consumed extends Component {
			render(props) {
				return <strong>{props.a}</strong>;
			}
		}

		sinon.spy(Consumed.prototype, 'render');

		class Outer extends Component {
			render() {
				return (
					<div>
						<Inner />
					</div>
				);
			}
		}

		class Inner extends Component {
			shouldComponentUpdate() {
				return false;
			}

			render() {
				return (
					<Fragment>
						<InnerMost />
					</Fragment>
				);
			}
		}

		class InnerMost extends Component {
			render() {
				return (
					<div>
						<Consumer>{data => <Consumed {...data} />}</Consumer>
					</div>
				);
			}
		}

		class App extends Component {
			render() {
				return (
					<Provider value={this.props.value}>
						<Outer />
					</Provider>
				);
			}
		}

		render(<App value={CONTEXT} />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div><div><strong>a</strong></div></div>'
		);
		expect(Consumed.prototype.render).to.have.been.calledOnce;

		render(<App value={UPDATED_CONTEXT} />, scratch);

		rerender();

		// initial render does not invoke anything but render():
		expect(Consumed.prototype.render).to.have.been.calledTwice;
		// expect(Consumed.prototype.render).to.have.been.calledWithMatch({ ...UPDATED_CONTEXT }, {}, { ['__cC' + (ctxId - 1)]: {} });
		expect(scratch.innerHTML).to.equal(
			'<div><div><strong>b</strong></div></div>'
		);
		setTimeout(() => {
			expect(Consumed.prototype.render).to.have.been.calledTwice;
			done();
		});
	});

	it('should keep the right context at the right "depth"', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { theme: 'a', global: 1 };
		const NESTED_CONTEXT = { theme: 'b', global: 1 };

		let receivedData;
		let receivedNestedData;

		class Inner extends Component {
			render(props) {
				return (
					<div>
						{props.theme} - {props.global}
					</div>
				);
			}
		}
		class Nested extends Component {
			render(props) {
				return (
					<div>
						{props.theme} - {props.global}
					</div>
				);
			}
		}

		sinon.spy(Inner.prototype, 'render');
		sinon.spy(Nested.prototype, 'render');

		render(
			<Provider value={CONTEXT}>
				<Provider value={NESTED_CONTEXT}>
					<Consumer>
						{data => {
							receivedNestedData = data;
							return <Nested {...data} />;
						}}
					</Consumer>
				</Provider>
				<Consumer>
					{data => {
						receivedData = data;
						return <Inner {...data} />;
					}}
				</Consumer>
			</Provider>,
			scratch
		);

		// initial render does not invoke anything but render():
		expect(Nested.prototype.render).to.have.been.calledWithMatch({
			...NESTED_CONTEXT
		});
		expect(Inner.prototype.render).to.have.been.calledWithMatch({ ...CONTEXT });
		expect(receivedData).to.equal(CONTEXT);
		expect(receivedNestedData).to.equal(NESTED_CONTEXT);

		expect(scratch.innerHTML).to.equal('<div>b - 1</div><div>a - 1</div>');
	});

	it("should not re-render the consumer if the context doesn't change", () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { i: 1 };

		class NoUpdate extends Component {
			shouldComponentUpdate() {
				return false;
			}

			render() {
				return this.props.children;
			}
		}

		class Inner extends Component {
			render(props) {
				return <div>{props.i}</div>;
			}
		}

		sinon.spy(Inner.prototype, 'render');

		render(
			<Provider value={CONTEXT}>
				<NoUpdate>
					<Consumer>{data => <Inner {...data} />}</Consumer>
				</NoUpdate>
			</Provider>,
			scratch
		);

		render(
			<Provider value={CONTEXT}>
				<NoUpdate>
					<Consumer>{data => <Inner {...data} />}</Consumer>
				</NoUpdate>
			</Provider>,
			scratch
		);

		// Rendered twice, should called just one 'Consumer' render
		expect(Inner.prototype.render).to.have.been.calledOnce.and.calledWithMatch(
			CONTEXT
		);
		expect(scratch.innerHTML).to.equal('<div>1</div>');

		act(() => {
			render(
				<Provider value={{ i: 2 }}>
					<NoUpdate>
						<Consumer>{data => <Inner {...data} />}</Consumer>
					</NoUpdate>
				</Provider>,
				scratch
			);
		});

		// Rendered three times, should call 'Consumer' render two times
		expect(Inner.prototype.render).to.have.been.calledTwice.and.calledWithMatch(
			{ i: 2 }
		);
		expect(scratch.innerHTML).to.equal('<div>2</div>');
	});

	it('should allow for updates of props', () => {
		let app;
		const { Provider, Consumer } = createContext(null);
		class App extends Component {
			constructor(props) {
				super(props);
				this.state = {
					status: 'initial'
				};

				this.renderInner = this.renderInner.bind(this);

				app = this;
			}

			renderInner(value) {
				return (
					<p>
						{value}: {this.state.status}
					</p>
				);
			}

			render() {
				return (
					<Provider value="value">
						<Consumer>{this.renderInner}</Consumer>
					</Provider>
				);
			}
		}

		act(() => {
			render(<App />, scratch);
		});

		expect(scratch.innerHTML).to.equal('<p>value: initial</p>');

		act(() => {
			app.setState({ status: 'updated' });
			rerender();
		});

		expect(scratch.innerHTML).to.equal('<p>value: updated</p>');
	});

	it('should re-render the consumer if the children change', () => {
		const { Provider, Consumer } = createContext(null);
		const CONTEXT = { i: 1 };

		class Inner extends Component {
			render(props) {
				return <div>{props.i}</div>;
			}
		}

		sinon.spy(Inner.prototype, 'render');

		act(() => {
			render(
				<Provider value={CONTEXT}>
					<Consumer>{data => <Inner {...data} />}</Consumer>
				</Provider>,
				scratch
			);

			// Not calling re-render since it's gonna get called with the same Consumer function
			render(
				<Provider value={CONTEXT}>
					<Consumer>{data => <Inner {...data} />}</Consumer>
				</Provider>,
				scratch
			);
		});

		// Rendered twice, with two different children for consumer, should render twice
		expect(Inner.prototype.render).to.have.been.calledTwice;
		expect(scratch.innerHTML).to.equal('<div>1</div>');
	});

	it('should not rerender consumers that have been unmounted', () => {
		const { Provider, Consumer } = createContext(0);

		const Inner = sinon.spy(props => <div>{props.value}</div>);

		let toggleConsumer;
		let changeValue;
		class App extends Component {
			constructor() {
				super();

				this.state = { value: 0, show: true };
				changeValue = value => this.setState({ value });
				toggleConsumer = () => this.setState(({ show }) => ({ show: !show }));
			}
			render(props, state) {
				return (
					<Provider value={state.value}>
						<div>
							{state.show ? (
								<Consumer>{data => <Inner value={data} />}</Consumer>
							) : null}
						</div>
					</Provider>
				);
			}
		}

		render(<App />, scratch);
		expect(scratch.innerHTML).to.equal('<div><div>0</div></div>');
		expect(Inner).to.have.been.calledOnce;

		changeValue(1);
		rerender();
		expect(scratch.innerHTML).to.equal('<div><div>1</div></div>');
		expect(Inner).to.have.been.calledTwice;

		toggleConsumer();
		rerender();
		expect(scratch.innerHTML).to.equal('<div></div>');
		expect(Inner).to.have.been.calledTwice;

		changeValue(2);
		rerender();
		expect(scratch.innerHTML).to.equal('<div></div>');
		expect(Inner).to.have.been.calledTwice;
	});

	describe('class.contextType', () => {
		it('should use default value', () => {
			const ctx = createContext('foo');

			let actual;
			class App extends Component {
				render() {
					actual = this.context;
					return <div>bar</div>;
				}
			}

			App.contextType = ctx;

			render(<App />, scratch);
			expect(actual).to.deep.equal('foo');
		});

		it('should use the value of the nearest Provider', () => {
			const ctx = createContext('foo');

			let actual;
			class App extends Component {
				render() {
					actual = this.context;
					return <div>bar</div>;
				}
			}

			App.contextType = ctx;
			const Provider = ctx.Provider;

			render(
				<Provider value="bar">
					<Provider value="bob">
						<App />
					</Provider>
				</Provider>,
				scratch
			);
			expect(actual).to.deep.equal('bob');
		});

		it('should restore legacy context for children', () => {
			const Foo = createContext('foo');
			const spy = sinon.spy();

			class NewContext extends Component {
				render() {
					return <div>{this.props.children}</div>;
				}
			}

			class OldContext extends Component {
				getChildContext() {
					return { foo: 'foo' };
				}

				render() {
					return <div>{this.props.children}</div>;
				}
			}

			class Inner extends Component {
				render() {
					spy(this.context);
					return <div>Inner</div>;
				}
			}

			NewContext.contextType = Foo;

			render(
				<Foo.Provider value="bar">
					<OldContext>
						<NewContext>
							<Inner />
						</NewContext>
					</OldContext>
				</Foo.Provider>,
				scratch
			);

			expect(spy).to.be.calledWithMatch({ foo: 'foo' });
		});

		it('should call componentWillUnmount', () => {
			let Foo = createContext('foo');
			let spy = sinon.spy();

			let instance;
			class App extends Component {
				constructor(props) {
					super(props);
					instance = this;
				}

				componentWillUnmount() {
					spy(this);
				}

				render() {
					return <div />;
				}
			}

			App.contextType = Foo;

			render(
				<Foo.Provider value="foo">
					<App />
				</Foo.Provider>,
				scratch
			);

			render(null, scratch);

			expect(spy).to.be.calledOnce;
			expect(spy.getCall(0).args[0]).to.equal(instance);
		});

		it('should order updates correctly', () => {
			const events = [];
			let update;
			const Store = createContext(null);

			class Root extends Component {
				constructor(props) {
					super(props);
					this.state = { id: 0 };
					update = this.updateStore = this.updateStore.bind(this);
				}

				updateStore() {
					this.setState(state => ({ id: state.id + 1 }));
				}

				render() {
					return (
						<Store.Provider value={this.state.id}>
							<App />
						</Store.Provider>
					);
				}
			}

			class App extends Component {
				shouldComponentUpdate() {
					return false;
				}

				render() {
					return <Store.Consumer>{id => <Parent key={id} />}</Store.Consumer>;
				}
			}

			function Parent(props) {
				return <Store.Consumer>{id => <Child id={id} />}</Store.Consumer>;
			}

			class Child extends Component {
				componentDidMount() {
					events.push('mount ' + this.props.id);
				}

				componentDidUpdate(prevProps) {
					events.push('update ' + prevProps.id + ' to ' + this.props.id);
				}

				componentWillUnmount() {
					events.push('unmount ' + this.props.id);
				}

				render() {
					events.push('render ' + this.props.id);
					return this.props.id;
				}
			}

			render(<Root />, scratch);
			expect(events).to.deep.equal(['render 0', 'mount 0']);

			update();
			rerender();
			expect(events).to.deep.equal([
				'render 0',
				'mount 0',
				'unmount 0',
				'render 1',
				'mount 1'
			]);
		});
	});

	it('should rerender when reset to defaultValue', () => {
		const defaultValue = { state: 'hi' };
		const context = createContext(defaultValue);
		let set;

		class NoUpdate extends Component {
			shouldComponentUpdate() {
				return false;
			}

			render() {
				return <context.Consumer>{v => <p>{v.state}</p>}</context.Consumer>;
			}
		}

		class Provider extends Component {
			constructor(props) {
				super(props);
				this.state = defaultValue;
				set = this.setState.bind(this);
			}

			render() {
				return (
					<context.Provider value={this.state}>
						<NoUpdate />
					</context.Provider>
				);
			}
		}

		render(<Provider />, scratch);
		expect(scratch.innerHTML).to.equal('<p>hi</p>');

		set({ state: 'bye' });
		rerender();
		expect(scratch.innerHTML).to.equal('<p>bye</p>');

		set(defaultValue);
		rerender();
		expect(scratch.innerHTML).to.equal('<p>hi</p>');
	});

	it('should not call sCU on context update', () => {
		const Ctx = createContext('foo');

		/** @type {(s: string) => void} */
		let update;
		class App extends Component {
			constructor(props) {
				super(props);
				this.state = { foo: 'foo' };
				update = v => this.setState({ foo: v });
			}
			render() {
				return (
					<Ctx.Provider value={this.state.foo}>
						<Child />
					</Ctx.Provider>
				);
			}
		}

		const spy = sinon.spy();

		class Child extends Component {
			static contextType = Ctx;

			shouldComponentUpdate() {
				spy();
				return false;
			}

			render() {
				return <p>{this.context}</p>;
			}
		}

		render(<App />, scratch);
		expect(scratch.textContent).to.equal('foo');

		update('bar');
		rerender();
		expect(scratch.textContent).to.equal('bar');
		expect(spy).not.to.be.called;
	});
});