Back to Repositories

Testing Child Array Transformation Utility in Preact

This test suite validates the toChildArray utility function in Preact, which handles the conversion and normalization of children elements in components. It thoroughly examines how different types of child elements are processed and flattened into a consistent array format.

Test Coverage Overview

The test suite provides comprehensive coverage of toChildArray functionality, examining various input scenarios and edge cases.

  • Empty, null, and boolean child handling
  • Single child processing (text, numbers, DOM nodes, components)
  • Multiple children handling
  • Array flattening and normalization
  • Non-renderable element filtering

Implementation Analysis

The testing approach utilizes Jest’s describe/it pattern combined with Preact’s rendering utilities. Each test case isolates specific child scenarios, from simple primitives to complex nested arrays.

The implementation leverages Preact’s createElement and render functions, with a wrapper component (Foo) to access transformed children arrays.

Technical Details

  • Testing Framework: Jest
  • Component Library: Preact
  • Test Utilities: setupScratch, teardown, getMixedArray
  • DOM Manipulation: HTMLDivElement scratch element
  • Assertion Library: Chai-style expect assertions

Best Practices Demonstrated

The test suite exemplifies robust testing practices through systematic organization and thorough validation.

  • Proper test isolation with beforeEach/afterEach hooks
  • Comprehensive edge case coverage
  • Consistent assertion patterns
  • Clear test case descriptions
  • Efficient test setup and teardown

preactjs/preact

test/browser/toChildArray.test.js

            
import { createElement, render, toChildArray } from 'preact';
import {
	setupScratch,
	teardown,
	getMixedArray,
	mixedArrayHTML
} from '../_util/helpers';

/** @jsx createElement */

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

	let children;

	let Foo = props => {
		children = toChildArray(props.children);
		return <div>{children}</div>;
	};

	let Bar = () => <span>Bar</span>;

	beforeEach(() => {
		scratch = setupScratch();
		children = undefined;
	});

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

	it('returns an empty array with no child', () => {
		render(<Foo />, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(0);
		expect(scratch.innerHTML).to.equal('<div></div>');
	});

	it('returns an empty array with null as a child', () => {
		render(<Foo>{null}</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(0);
		expect(scratch.innerHTML).to.equal('<div></div>');
	});

	it('returns an empty array with false as a child', () => {
		render(<Foo>{false}</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(0);
		expect(scratch.innerHTML).to.equal('<div></div>');
	});

	it('returns an empty array with true as a child', () => {
		render(<Foo>{true}</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(0);
		expect(scratch.innerHTML).to.equal('<div></div>');
	});

	it('should skip a function child', () => {
		const child = num => num.toFixed(2);
		render(<Foo>{child}</Foo>, scratch);
		expect(children).to.be.an('array');
		expect(scratch.innerHTML).to.equal('<div></div>');
	});

	it('returns an array containing a VNode with a text child', () => {
		render(<Foo>text</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(1);
		expect(children[0]).to.equal('text');
		expect(scratch.innerHTML).to.equal('<div>text</div>');
	});

	it('returns an array containing a VNode with a number child', () => {
		render(<Foo>{1}</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(1);
		expect(children[0]).to.equal(1);
		expect(scratch.innerHTML).to.equal('<div>1</div>');
	});

	it('returns an array containing a VNode with a DOM node child', () => {
		render(
			<Foo>
				<span />
			</Foo>,
			scratch
		);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(1);
		expect(children[0].type).to.equal('span');
		expect(scratch.innerHTML).to.equal('<div><span></span></div>');
	});

	it('returns an array containing a VNode with a Component child', () => {
		render(
			<Foo>
				<Bar />
			</Foo>,
			scratch
		);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(1);
		expect(children[0].type).to.equal(Bar);
		expect(scratch.innerHTML).to.equal('<div><span>Bar</span></div>');
	});

	it('returns an array with multiple children', () => {
		render(
			<Foo>
				0<span />
				<input />
				<div />1
			</Foo>,
			scratch
		);

		expect(children).to.be.an('array');
		expect(children[0]).to.equal('0');
		expect(children[1].type).to.equal('span');
		expect(children[2].type).to.equal('input');
		expect(children[3].type).to.equal('div');
		expect(children[4]).to.equal('1');
		expect(scratch.innerHTML).to.equal(
			`<div>0<span></span><input><div></div>1</div>`
		);
	});

	it('returns an array with non-renderables removed with a mixed array as children', () => {
		const mixedArray = getMixedArray();
		render(<Foo>{mixedArray}</Foo>, scratch);

		expect(children).to.be.an('array');
		expect(children).to.have.lengthOf(8); // Length of flattened mixedArray with non-renderables removed
		expect(scratch.innerHTML).to.equal(`<div>${mixedArrayHTML}</div>`);

		function filterAndReduceChildren(acc, child) {
			if (Array.isArray(child)) {
				return child.reduce(filterAndReduceChildren, acc);
			}

			if (
				child != null &&
				typeof child != 'boolean' &&
				typeof child != 'function'
			) {
				acc.push(child);
			}

			return acc;
		}

		let renderableArray = filterAndReduceChildren([], mixedArray);

		expect(children).to.have.lengthOf(renderableArray.length);

		for (let i = 0; i < renderableArray.length; i++) {
			let originalChild = renderableArray[i];
			let actualChild = children[i];

			if (
				typeof originalChild == 'string' ||
				typeof originalChild == 'number'
			) {
				expect(actualChild).to.equal(originalChild);
			} else {
				expect(actualChild.type).to.equal(originalChild.type);
			}
		}
	});

	it('flattens sibling and nested arrays', () => {
		const list1 = [0, 1];
		const list2 = [2, 3];
		const list3 = [4, 5];
		const list4 = [6, 7];
		const list5 = [8, 9];

		const flatList = [...list1, ...list2, ...list3, ...list4, ...list5];

		render(
			<Foo>
				{[list1, list2]}
				{[list3, list4]}
				{list5}
			</Foo>,
			scratch
		);

		expect(children).to.be.an('array');
		expect(scratch.innerHTML).to.equal('<div>0123456789</div>');

		for (let i = 0; i < flatList.length; i++) {
			expect(children[i]).to.equal(flatList[i]);
		}
	});
});