@wordpress/element
Version:
Element React module for WordPress.
730 lines (561 loc) • 17.7 kB
JavaScript
/* 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 & friends <img/>' );
expect( result ).toBe( 'hello & world & friends <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><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("foo.png")" href="/index.php?foo=bar&qux=<"scary">"'
);
} );
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 */