Back to Repositories

Validating SVG Component Rendering in PreactJS

This test suite validates SVG rendering and attribute handling in Preact components. It ensures proper SVG namespace management, attribute transformation, and DOM integration while verifying both static and dynamic SVG content rendering.

Test Coverage Overview

The test suite provides comprehensive coverage of SVG functionality in Preact:

  • SVG rendering to string and DOM
  • Namespace URI handling and inheritance
  • SVG attribute support including xlink:href
  • Class and className attribute handling
  • ForeignObject element behavior
  • DOM-SVG transition scenarios

Implementation Analysis

The testing approach utilizes Jest’s describe/it blocks with focused component rendering scenarios. It implements setUp/tearDown patterns through beforeEach/afterEach hooks and uses the setupScratch utility for DOM manipulation. The tests verify both static SVG rendering and dynamic component updates through state changes.

Technical Details

Testing tools and configuration include:

  • Jest as the test runner
  • Preact test-utils for rerender functionality
  • Custom helper utilities for scratch element management
  • DOM manipulation through createElement and render methods
  • Attribute sorting utilities for consistent comparisons

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices:

  • Isolated test cases with proper setup/teardown
  • Comprehensive edge case coverage
  • Consistent attribute handling verification
  • Namespace validation across different contexts
  • Clean separation of concerns in test organization

preactjs/preact

test/browser/svg.test.js

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

/** @jsx createElement */

describe('svg', () => {
	let scratch;

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

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

	it('should render SVG to string', () => {
		render(
			<svg viewBox="0 0 360 360">
				<path
					stroke="white"
					fill="black"
					d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z"
				/>
			</svg>,
			scratch
		);

		let html = sortAttributes(
			String(scratch.innerHTML).replace(
				' xmlns="http://www.w3.org/2000/svg"',
				''
			)
		);
		expect(html).to.equal(
			sortAttributes(
				`
			<svg viewBox="0 0 360 360">
				<path d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" fill="black" stroke="white"></path>
			</svg>
		`.replace(/[\n\t]+/g, '')
			)
		);
	});

	it('should support svg xlink:href attribute', () => {
		render(
			createElement('svg', {}, createElement('use', { 'xlink:href': '#foo' })),
			scratch
		);

		expect(scratch.innerHTML).to.contain(` href="#foo"`);
	});

	it('should support svg attributes', () => {
		const Demo = ({ url }) => (
			<svg viewBox="0 0 360 360" xlinkHref={url}>
				<path
					d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z"
					fill="black"
					stroke="white"
				/>
			</svg>
		);
		render(<Demo url="www.preact.com" />, scratch);

		let html = String(scratch.innerHTML).replace(
			' xmlns="http://www.w3.org/2000/svg"',
			''
		);
		html = sortAttributes(
			html.replace(' xmlns:xlink="http://www.w3.org/1999/xlink"', '')
		);
		expect(html).to.equal(
			sortAttributes(
				`
			<svg viewBox="0 0 360 360" href="www.preact.com">
				<path d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" fill="black" stroke="white"></path>
			</svg>
		`.replace(/[\n\t]+/g, '')
			)
		);
		render(<Demo />, scratch);

		html = String(scratch.innerHTML).replace(
			' xmlns="http://www.w3.org/2000/svg"',
			''
		);
		html = sortAttributes(
			html.replace(' xmlns:xlink="http://www.w3.org/1999/xlink"', '')
		);
		expect(html).to.equal(
			sortAttributes(
				`
			<svg viewBox="0 0 360 360">
				<path d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" fill="black" stroke="white"></path>
			</svg>
		`.replace(/[\n\t]+/g, '')
			)
		);
	});

	it('should render SVG to DOM', () => {
		const Demo = () => (
			<svg viewBox="0 0 360 360">
				<path
					d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z"
					fill="black"
					stroke="white"
				/>
			</svg>
		);
		render(<Demo />, scratch);

		let html = sortAttributes(
			String(scratch.innerHTML).replace(
				' xmlns="http://www.w3.org/2000/svg"',
				''
			)
		);
		expect(html).to.equal(
			sortAttributes(
				'<svg viewBox="0 0 360 360"><path stroke="white" fill="black" d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z"></path></svg>'
			)
		);
	});

	it('should render with the correct namespace URI', () => {
		render(<svg />, scratch);

		let namespace = scratch.querySelector('svg').namespaceURI;

		expect(namespace).to.equal('http://www.w3.org/2000/svg');
	});

	it('should render children with the correct namespace URI', () => {
		render(
			<svg>
				<text />
			</svg>,
			scratch
		);

		let namespace = scratch.querySelector('text').namespaceURI;

		expect(namespace).to.equal('http://www.w3.org/2000/svg');
	});

	it('should inherit correct namespace URI from parent', () => {
		const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		scratch.appendChild(svg);

		render(<text />, scratch.firstChild);

		let namespace = scratch.querySelector('text').namespaceURI;

		expect(namespace).to.equal('http://www.w3.org/2000/svg');
	});

	it('should inherit correct namespace URI from parent upon updating', () => {
		setupRerender();

		const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		scratch.appendChild(svg);

		class App extends Component {
			state = { show: true };
			componentDidMount() {
				// eslint-disable-next-line
				this.setState({ show: false }, () => {
					expect(scratch.querySelector('circle').namespaceURI).to.equal(
						'http://www.w3.org/2000/svg'
					);
				});
			}
			render() {
				return this.state.show ? <text /> : <circle />;
			}
		}

		render(<App />, scratch.firstChild);
	});

	it('should use attributes for className', () => {
		const Demo = ({ c }) => (
			<svg viewBox="0 0 360 360" {...(c ? { class: 'foo_' + c } : {})}>
				<path
					class={c && 'bar_' + c}
					stroke="white"
					fill="black"
					d="M347.1 357.9L183.3 256.5 13 357.9V1.7h334.1v356.2zM58.5 47.2v231.4l124.8-74.1 118.3 72.8V47.2H58.5z"
				/>
			</svg>
		);
		render(<Demo c="1" />, scratch);
		let root = scratch.firstChild;
		sinon.spy(root, 'removeAttribute');
		render(<Demo />, scratch);
		expect(root.removeAttribute).to.have.been.calledOnce.and.calledWith(
			'class'
		);

		root.removeAttribute.restore();

		render(<div />, scratch);
		render(<Demo />, scratch);
		root = scratch.firstChild;
		sinon.spy(root, 'setAttribute');
		render(<Demo c="2" />, scratch);
		expect(root.setAttribute).to.have.been.calledOnce.and.calledWith(
			'class',
			'foo_2'
		);

		root.setAttribute.restore();
	});

	it('should still support class attribute', () => {
		render(<svg viewBox="0 0 1 1" class="foo bar" />, scratch);

		expect(scratch.innerHTML).to.contain(` class="foo bar"`);
	});

	it('should still support className attribute', () => {
		render(<svg viewBox="0 0 1 1" className="foo bar" />, scratch);

		expect(scratch.innerHTML).to.contain(` class="foo bar"`);
	});

	it('should switch back to HTML for <foreignObject>', () => {
		render(
			<svg>
				<g>
					<foreignObject>
						<a href="#foo">test</a>
					</foreignObject>
				</g>
			</svg>,
			scratch
		);

		expect(scratch.getElementsByTagName('a'))
			.to.have.property('0')
			.that.is.a('HTMLAnchorElement');
	});

	it('should render foreignObject as an svg element', () => {
		render(
			<svg>
				<g>
					<foreignObject>
						<a href="#foo">test</a>
					</foreignObject>
				</g>
			</svg>,
			scratch
		);

		expect(scratch.querySelector('foreignObject').localName).to.equal(
			'foreignObject'
		);
	});

	it('should transition from DOM to SVG and back', () => {
		render(
			<div>
				<svg
					id="svg1923"
					width="700"
					xmlns="http://www.w3.org/2000/svg"
					height="700"
				>
					<circle cy="333" cx="333" r="333" />
					<circle cy="333" cx="333" r="333" fill="#fede58" />
				</svg>
			</div>,
			scratch
		);

		expect(scratch.firstChild).to.be.an('HTMLDivElement');
		expect(scratch.firstChild.firstChild).to.be.an('SVGSVGElement');
	});
});