UNPKG

@wordpress/element

Version:
730 lines (561 loc) 17.7 kB
/* eslint-disable testing-library/render-result-naming-convention */ /** * Internal dependencies */ import { Component, createContext, createElement, Fragment, StrictMode, forwardRef, } from '../react'; import RawHTML from '../raw-html'; import serialize, { hasPrefix, renderElement, renderNativeComponent, renderComponent, renderAttributes, renderStyle, } from '../serialize'; const noop = () => {}; describe( 'serialize()', () => { it( 'should allow only valid attribute names', () => { const element = createElement( 'div', { 'notok\u007F': 'bad', 'notok"': 'bad', ok: 'good', 'notok\uFDD0': 'bad', } ); const result = serialize( element ); expect( result ).toBe( '<div ok="good"></div>' ); } ); it( 'should render with context (legacy)', () => { class Provider extends Component { getChildContext() { return { greeting: 'Hello!', }; } render() { return this.props.children; } } Provider.childContextTypes = { greeting: noop, }; // NOTE: Technically, a component should only receive context if it // explicitly defines `contextTypes`. This requirement is ignored in // our implementation. function FunctionComponent( props, context ) { return 'FunctionComponent: ' + context.greeting; } class ClassComponent extends Component { render() { return 'ClassComponent: ' + this.context.greeting; } } const result = serialize( <Provider> <FunctionComponent /> <ClassComponent /> </Provider> ); expect( result ).toBe( 'FunctionComponent: Hello!' + 'ClassComponent: Hello!' ); } ); it( 'should render with forwardRef', () => { const ForwardedComponent = forwardRef( () => { return <div>test</div>; } ); const result = serialize( <ForwardedComponent /> ); expect( result ).toBe( '<div>test</div>' ); } ); describe( 'empty attributes', () => { it( 'should not render a null attribute value', () => { const result = serialize( <video src={ undefined } /> ); expect( result ).toBe( '<video></video>' ); } ); it( 'should not render an undefined attribute value', () => { const result = serialize( <video src={ null } /> ); expect( result ).toBe( '<video></video>' ); } ); it( 'should an explicitly empty string attribute', () => { const result = serialize( <video className="" /> ); expect( result ).toBe( '<video class=""></video>' ); } ); it( 'should not render an empty object style', () => { const result = serialize( <video style={ {} } /> ); expect( result ).toBe( '<video></video>' ); } ); } ); describe( 'boolean attributes', () => { it( 'should render elements with false boolean attributes', () => { [ false, null, undefined ].forEach( ( controls ) => { const result = serialize( <video src="/" controls={ controls } /> ); expect( result ).toBe( '<video src="/"></video>' ); } ); } ); it( 'should render elements with true boolean attributes', () => { [ true, 'true', 'false', '' ].forEach( ( controls ) => { const result = serialize( <video src="/" controls={ controls } /> ); expect( result ).toBe( '<video src="/" controls></video>' ); } ); } ); it( 'should not render non-boolean-attribute with boolean value', () => { const result = serialize( <video src controls /> ); expect( result ).toBe( '<video controls></video>' ); } ); } ); } ); describe( 'hasPrefix()', () => { it( 'returns true if prefixed', () => { const result = hasPrefix( 'Hello World', [ 'baz', 'Hello' ] ); expect( result ).toBe( true ); } ); it( 'returns false if not contains', () => { const result = hasPrefix( 'World', [ 'Hello' ] ); expect( result ).toBe( false ); } ); it( 'returns false if contains but not prefix', () => { const result = hasPrefix( 'World Hello', [ 'Hello' ] ); expect( result ).toBe( false ); } ); } ); describe( 'renderElement()', () => { it( 'renders empty content as empty string', () => { [ null, undefined, false ].forEach( ( element ) => { const result = renderElement( element ); expect( result ).toBe( '' ); } ); } ); it( 'renders an array of mixed content', () => { const result = renderElement( [ 'hello', <div key="div" /> ] ); expect( result ).toBe( 'hello<div></div>' ); } ); it( 'SVG attributes with dashes should be rendered as such - even with wrong casing', () => { const result = renderElement( <svg> <rect x="0" y="0" strokeWidth="5" STROKELinejoin="miter"></rect> </svg> ); expect( result ).toBe( '<svg><rect x="0" y="0" stroke-width="5" stroke-linejoin="miter"></rect></svg>' ); } ); it( 'Case sensitive attributes should have the right casing - even with wrong casing', () => { const result = renderElement( <svg ViEWBOx="0 0 1 1" preserveAsPECTRatio="slice"></svg> ); expect( result ).toBe( '<svg viewBox="0 0 1 1" preserveAspectRatio="slice"></svg>' ); } ); it( 'SVG attributes with colons should be rendered as such - even with wrong casing', () => { const result = renderElement( <svg viewBox="0 0 1 1" XLINKROLE="some-role" xlinkShow="hello" ></svg> ); expect( result ).toBe( '<svg viewBox="0 0 1 1" xlink:role="some-role" xlink:show="hello"></svg>' ); } ); it( 'renders escaped string element', () => { const result = renderElement( 'hello & world &amp; friends <img/>' ); expect( result ).toBe( 'hello &amp; world &amp; friends &lt;img/>' ); } ); it( 'renders numeric element as string', () => { const result = renderElement( 10 ); expect( result ).toBe( '10' ); } ); it( 'renders native component', () => { const result = renderElement( <div className="greeting">Hello</div> ); expect( result ).toBe( '<div class="greeting">Hello</div>' ); } ); it( 'renders function component', () => { function Greeting() { return <div className="greeting">Hello</div>; } const result = renderElement( <Greeting /> ); expect( result ).toBe( '<div class="greeting">Hello</div>' ); } ); it( 'renders class component', () => { class Greeting extends Component { render() { return <div className="greeting">Hello</div>; } } const result = renderElement( <Greeting /> ); expect( result ).toBe( '<div class="greeting">Hello</div>' ); } ); it( 'renders empty string for indeterminite types', () => { const result = renderElement( {} ); expect( result ).toBe( '' ); } ); it( 'renders Fragment as its inner children', () => { const result = renderElement( <Fragment>Hello</Fragment> ); expect( result ).toBe( 'Hello' ); } ); it( 'renders StrictMode with undefined children', () => { const result = renderElement( <StrictMode /> ); expect( result ).toBe( '' ); } ); it( 'renders StrictMode as its inner children', () => { const result = renderElement( <StrictMode>Hello</StrictMode> ); expect( result ).toBe( 'Hello' ); } ); it( 'renders Fragment with undefined children', () => { const result = renderElement( <Fragment /> ); expect( result ).toBe( '' ); } ); it( 'renders default value from Context API', () => { const { Consumer } = createContext( { value: 'default', } ); const result = renderElement( <Consumer>{ ( context ) => context.value }</Consumer> ); expect( result ).toBe( 'default' ); } ); it( 'renders provided value through Context API', () => { const { Consumer, Provider } = createContext( { value: 'default', } ); const result = renderElement( <Provider value={ { value: 'provided' } }> <Consumer>{ ( context ) => context.value }</Consumer> </Provider> ); expect( result ).toBe( 'provided' ); } ); it( 'renders proper value through Context API when multiple providers present', () => { const { Consumer, Provider } = createContext( { value: 'default', } ); const result = renderElement( <Fragment> <Provider value={ { value: '1st provided' } }> <Consumer>{ ( context ) => context.value }</Consumer> </Provider> | <Provider value={ { value: '2nd provided' } }> <Consumer>{ ( context ) => context.value }</Consumer> </Provider> |<Consumer>{ ( context ) => context.value }</Consumer> </Fragment> ); expect( result ).toBe( '1st provided|2nd provided|default' ); } ); it( 'renders proper value through Context API when nested providers present', () => { const { Consumer, Provider } = createContext( { value: 'default', } ); const result = renderElement( <Provider value={ { value: 'outer provided' } }> <Provider value={ { value: 'inner provided' } }> <Consumer>{ ( context ) => context.value }</Consumer> </Provider> |<Consumer>{ ( context ) => context.value }</Consumer> </Provider> ); expect( result ).toBe( 'inner provided|outer provided' ); } ); it( 'renders RawHTML as its unescaped children', () => { const result = renderElement( <RawHTML>{ '<img/>' }</RawHTML> ); expect( result ).toBe( '<img/>' ); } ); it( 'renders RawHTML with wrapper if props passed', () => { const result = renderElement( <RawHTML className="foo">{ '<img/>' }</RawHTML> ); expect( result ).toBe( '<div class="foo"><img/></div>' ); } ); it( 'renders RawHTML with empty children as empty string', () => { const result = renderElement( <RawHTML /> ); expect( result ).toBe( '' ); } ); it( 'renders RawHTML with wrapper and empty children', () => { const result = renderElement( <RawHTML className="foo" /> ); expect( result ).toBe( '<div class="foo"></div>' ); } ); } ); describe( 'renderNativeComponent()', () => { describe( 'textarea', () => { it( 'should render textarea value as its content', () => { const result = renderNativeComponent( 'textarea', { value: 'Hello', children: [], } ); expect( result ).toBe( '<textarea>Hello</textarea>' ); } ); it( 'should render textarea children as its content', () => { const result = renderNativeComponent( 'textarea', { children: [ 'Hello' ], } ); expect( result ).toBe( '<textarea>Hello</textarea>' ); } ); } ); describe( 'escaping', () => { it( 'should escape children', () => { const result = renderNativeComponent( 'div', { children: [ '<img/>' ], } ); expect( result ).toBe( '<div>&lt;img/></div>' ); } ); it( 'should not render invalid dangerouslySetInnerHTML', () => { const result = renderNativeComponent( 'div', { dangerouslySetInnerHTML: { __html: undefined }, } ); expect( result ).toBe( '<div></div>' ); } ); it( 'should not escape children with dangerouslySetInnerHTML', () => { const result = renderNativeComponent( 'div', { dangerouslySetInnerHTML: { __html: '<img/>' }, } ); expect( result ).toBe( '<div><img/></div>' ); } ); } ); describe( 'self-closing', () => { it( 'should render self-closing elements', () => { const result = renderNativeComponent( 'img', { src: 'foo.png' } ); expect( result ).toBe( '<img src="foo.png"/>' ); } ); it( 'should ignore self-closing elements children', () => { const result = renderNativeComponent( 'img', { src: 'foo.png', children: [ 'Surprise!' ], } ); expect( result ).toBe( '<img src="foo.png"/>' ); } ); } ); describe( 'with children', () => { it( 'should render single literal child', () => { const result = renderNativeComponent( 'div', { children: 'Hello', } ); expect( result ).toBe( '<div>Hello</div>' ); } ); it( 'should render array of children', () => { const result = renderNativeComponent( 'div', { children: [ 'Hello ', <Fragment key="toWhom">World</Fragment> ], } ); expect( result ).toBe( '<div>Hello World</div>' ); } ); } ); } ); describe( 'renderComponent()', () => { it( 'calls constructor', () => { class Example extends Component { constructor() { super( ...arguments ); this.constructed = 'constructed'; } render() { return this.constructed; } } const result = renderComponent( Example, {} ); expect( result ).toBe( 'constructed' ); } ); it( 'does not call componentDidMount', () => { class Example extends Component { constructor() { super( ...arguments ); this.state = {}; } componentDidMount() { this.setState( { didMounted: 'didMounted' } ); } render() { return this.state.didMounted; } } const result = renderComponent( Example, {} ); expect( result ).toBe( '' ); } ); } ); describe( 'renderAttributes()', () => { describe( 'boolean attributes', () => { it( 'should return boolean attributes false as omitted', () => { const result = renderAttributes( { controls: false } ); expect( result ).toBe( '' ); } ); it( 'should return boolean attributes non-false as present', () => { [ true, 'true', 'false', '' ].forEach( ( controls ) => { const result = renderAttributes( { controls } ); expect( result ).toBe( ' controls' ); } ); } ); it( 'should consider normalized boolean attribute name', () => { const result = renderAttributes( { allowFullscreen: true } ); expect( result ).toBe( ' allowfullscreen' ); } ); } ); describe( 'prefixed attributes', () => { it( 'should not render if nullish', () => { [ null, undefined ].forEach( ( value ) => { const result = renderAttributes( { 'data-foo': value } ); expect( result ).toBe( '' ); } ); } ); it( 'should return in its string form unmodified', () => { let result = renderAttributes( { 'aria-hidden': '', } ); expect( result ).toBe( ' aria-hidden=""' ); result = renderAttributes( { 'aria-hidden': true, } ); expect( result ).toBe( ' aria-hidden="true"' ); result = renderAttributes( { 'aria-hidden': false, } ); expect( result ).toBe( ' aria-hidden="false"' ); } ); } ); describe( 'normalized attribute names', () => { it( 'should return with normal attribute names', () => { const result = renderAttributes( { htmlFor: 'foo', className: 'bar', contentEditable: true, } ); expect( result ).toBe( ' for="foo" class="bar" contenteditable="true"' ); } ); } ); describe( 'string escaping', () => { it( 'should escape string attributes', () => { const result = renderAttributes( { style: { background: 'url("foo.png")', }, href: '/index.php?foo=bar&qux=<"scary">', } ); expect( result ).toBe( ' style="background:url(&quot;foo.png&quot;)" href="/index.php?foo=bar&amp;qux=<&quot;scary&quot;&gt;"' ); } ); it( 'should render numeric attributes', () => { const result = renderAttributes( { size: 10, } ); expect( result ).toBe( ' size="10"' ); } ); } ); describe( 'ignored attributes', () => { it( 'does not render nullish attributes', () => { const result = renderAttributes( { className: null, htmlFor: undefined, } ); expect( result ).toBe( '' ); } ); it( 'does not render attributes of invalid types', () => { const result = renderAttributes( { onClick: () => {}, className: [], } ); expect( result ).toBe( '' ); } ); it( 'does not render internal attributes', () => { const result = renderAttributes( { key: 'foo', children: [ 'hello' ], } ); expect( result ).toBe( '' ); } ); } ); } ); describe( 'renderStyle()', () => { it( 'should return string verbatim', () => { const result = renderStyle( 'color:red' ); expect( result ).toBe( 'color:red' ); } ); it( 'should return undefined if empty', () => { const result = renderStyle( {} ); expect( result ).toBe( undefined ); } ); it( 'should render without trailing semi-colon', () => { const result = renderStyle( { color: 'red', } ); expect( result ).toBe( 'color:red' ); } ); it( 'should not render nullish value', () => { const result = renderStyle( { border: null, backgroundColor: undefined, color: 'red', } ); expect( result ).toBe( 'color:red' ); } ); it( 'should render a semi-colon delimited set', () => { const result = renderStyle( { color: 'red', border: '1px dotted green', } ); expect( result ).toBe( 'color:red;border:1px dotted green' ); } ); it( 'should kebab-case style properties', () => { const result = renderStyle( { color: 'red', backgroundColor: 'green', } ); expect( result ).toBe( 'color:red;background-color:green' ); } ); it( 'should not kebab-case custom properties', () => { const result = renderStyle( { '--myBackgroundColor': 'palegoldenrod', } ); expect( result ).toBe( '--myBackgroundColor:palegoldenrod' ); } ); it( 'should -kebab-case style properties with a vendor prefix', () => { const result = renderStyle( { msTransform: 'none', OTransform: 'none', MozTransform: 'none', WebkitTransform: 'none', } ); expect( result ).toBe( '-ms-transform:none;-o-transform:none;-moz-transform:none;-webkit-transform:none' ); } ); describe( 'value unit', () => { it( 'should not render zero unit', () => { const result = renderStyle( { borderWidth: 0, } ); expect( result ).toBe( 'border-width:0' ); } ); it( 'should render numeric units', () => { const result = renderStyle( { borderWidth: 10, } ); expect( result ).toBe( 'border-width:10px' ); } ); it( 'should not render numeric units for unitless properties', () => { const result = renderStyle( { order: 10, } ); expect( result ).toBe( 'order:10' ); } ); it( 'should not render numeric units for CSS custom properties', () => { const result = renderStyle( { '--myOrder': 10, } ); expect( result ).toBe( '--myOrder:10' ); } ); } ); } ); /* eslint-enable testing-library/render-result-naming-convention */