Back to Repositories

Testing Suspense Debug Functionality in PreactJS

This test suite validates Preact’s debug functionality with Suspense components, focusing on error handling and PropTypes validation. It ensures proper component lazy loading behavior and verifies warning/error mechanisms in development mode.

Test Coverage Overview

The test suite provides comprehensive coverage of Preact’s Suspense and lazy loading functionality.

Key areas tested include:
  • Missing Suspense component detection
  • Lazy component loading behavior
  • PropTypes validation within lazy-loaded components
  • Error and warning message handling
Integration points focus on the interaction between Suspense, lazy loading, and Preact’s debug utilities.

Implementation Analysis

The testing approach utilizes Jest’s describe/it pattern with setup/teardown hooks for consistent test environments. The implementation leverages Preact’s test utilities for rendering and rerendering components.

Technical patterns include:
  • Promise-based lazy loading verification
  • Error boundary testing
  • Component prop validation
  • Asynchronous component rendering

Technical Details

Testing tools and setup:
  • Jest test framework
  • Preact/compat for component rendering
  • Preact/debug for development mode features
  • Preact/test-utils for rerender functionality
  • Sinon for console mocking
  • Custom scratch element setup for DOM testing

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through systematic validation of component behavior.

Notable practices include:
  • Isolated test environments with proper cleanup
  • Comprehensive error case coverage
  • Async testing patterns
  • Console warning/error validation
  • Component display name verification

preactjs/preact

debug/test/browser/debug-suspense.test.js

            
import { createElement, render, lazy, Suspense } from 'preact/compat';
import 'preact/debug';
import { setupRerender } from 'preact/test-utils';
import {
	setupScratch,
	teardown,
	serializeHtml
} from '../../../test/_util/helpers';

/** @jsx createElement */

describe('debug with suspense', () => {
	/** @type {HTMLDivElement} */
	let scratch;
	let rerender;
	let errors = [];
	let warnings = [];

	beforeEach(() => {
		errors = [];
		warnings = [];
		scratch = setupScratch();
		rerender = setupRerender();
		sinon.stub(console, 'error').callsFake(e => errors.push(e));
		sinon.stub(console, 'warn').callsFake(w => warnings.push(w));
	});

	afterEach(() => {
		console.error.restore();
		console.warn.restore();
		teardown(scratch);
	});

	it('should throw on missing <Suspense>', () => {
		function Foo() {
			throw Promise.resolve();
		}

		expect(() => render(<Foo />, scratch)).to.throw;
	});

	it('should throw an error when using lazy and missing Suspense', () => {
		const Foo = () => <div>Foo</div>;
		const LazyComp = lazy(
			() => new Promise(resolve => resolve({ default: Foo }))
		);
		const fn = () => {
			render(<LazyComp />, scratch);
		};

		expect(fn).to.throw(/Missing Suspense/gi);
	});

	describe('PropTypes', () => {
		it('should validate propTypes inside lazy()', () => {
			function Baz(props) {
				return <h1>{props.unhappy}</h1>;
			}

			Baz.propTypes = {
				unhappy: function alwaysThrows(obj, key) {
					if (obj[key] === 'signal') throw Error('got prop inside lazy()');
				}
			};

			const loader = Promise.resolve({ default: Baz });
			const LazyBaz = lazy(() => loader);

			const suspense = (
				<Suspense fallback={<div>fallback...</div>}>
					<LazyBaz unhappy="signal" />
				</Suspense>
			);
			render(suspense, scratch);
			rerender(); // render fallback

			expect(console.error).to.not.be.called;
			expect(serializeHtml(scratch)).to.equal('<div>fallback...</div>');

			return loader.then(() => {
				rerender();
				expect(errors.length).to.equal(1);
				expect(errors[0].includes('got prop')).to.equal(true);
				expect(serializeHtml(scratch)).to.equal('<h1>signal</h1>');
			});
		});

		describe('warn for PropTypes on lazy()', () => {
			it('should log the function name', () => {
				const loader = Promise.resolve({
					default: function MyLazyLoaded() {
						return <div>Hi there</div>;
					}
				});
				const FakeLazy = lazy(() => loader);
				FakeLazy.propTypes = {};
				const suspense = (
					<Suspense fallback={<div>fallback...</div>}>
						<FakeLazy />
					</Suspense>
				);
				render(suspense, scratch);
				rerender(); // Render fallback

				expect(serializeHtml(scratch)).to.equal('<div>fallback...</div>');

				return loader.then(() => {
					rerender();
					expect(console.warn).to.be.calledTwice;
					expect(warnings[1].includes('MyLazyLoaded')).to.equal(true);
					expect(serializeHtml(scratch)).to.equal('<div>Hi there</div>');
				});
			});

			it('should log the displayName', () => {
				function MyLazyLoadedComponent() {
					return <div>Hi there</div>;
				}
				MyLazyLoadedComponent.displayName = 'HelloLazy';
				const loader = Promise.resolve({ default: MyLazyLoadedComponent });
				const FakeLazy = lazy(() => loader);
				FakeLazy.propTypes = {};
				const suspense = (
					<Suspense fallback={<div>fallback...</div>}>
						<FakeLazy />
					</Suspense>
				);
				render(suspense, scratch);
				rerender(); // Render fallback

				expect(serializeHtml(scratch)).to.equal('<div>fallback...</div>');

				return loader.then(() => {
					rerender();
					expect(console.warn).to.be.calledTwice;
					expect(warnings[1].includes('HelloLazy')).to.equal(true);
					expect(serializeHtml(scratch)).to.equal('<div>Hi there</div>');
				});
			});

			it("should not log a component if lazy loader's Promise rejects", () => {
				const loader = Promise.reject(new Error('Hey there'));
				const FakeLazy = lazy(() => loader);
				FakeLazy.propTypes = {};
				render(
					<Suspense fallback={<div>fallback...</div>}>
						<FakeLazy />
					</Suspense>,
					scratch
				);
				rerender(); // Render fallback

				expect(serializeHtml(scratch)).to.equal('<div>fallback...</div>');

				return loader.catch(() => {
					try {
						rerender();
					} catch (e) {
						// Ignore the loader's bubbling error
					}

					// Called once on initial render, and again when promise rejects
					expect(console.warn).to.be.calledTwice;
				});
			});

			it("should not log a component if lazy's loader throws", () => {
				const FakeLazy = lazy(() => {
					throw new Error('Hello');
				});
				FakeLazy.propTypes = {};
				let error;
				try {
					render(
						<Suspense fallback={<div>fallback...</div>}>
							<FakeLazy />
						</Suspense>,
						scratch
					);
				} catch (e) {
					error = e;
				}

				expect(console.warn).to.be.calledOnce;
				expect(error).not.to.be.undefined;
				expect(error.message).to.eql('Hello');
			});
		});
	});
});