@wordpress/blocks
Version:
Block API for WordPress.
772 lines (624 loc) • 20.4 kB
JavaScript
/**
* Internal dependencies
*/
import {
isValidCharacterReference,
DecodeEntityParser,
getTextPiecesSplitOnWhitespace,
getTextWithCollapsedWhitespace,
getMeaningfulAttributePairs,
isEquivalentTextTokens,
getNormalizedLength,
getNormalizedStyleValue,
getStyleProperties,
isEqualAttributesOfName,
isEqualTagAttributePairs,
isEqualTokensOfType,
getNextNonWhitespaceToken,
isEquivalentHTML,
validateBlock,
isClosedByToken,
} from '../validation';
import { createLogger } from '../validation/logger';
import {
registerBlockType,
unregisterBlockType,
getBlockTypes,
} from '../registration';
describe( 'validation', () => {
const defaultBlockSettings = {
save: ( { attributes } ) => attributes.fruit,
category: 'text',
title: 'block title',
};
beforeAll( () => {
// Initialize the block store.
require( '../../store' );
} );
afterEach( () => {
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );
describe( 'isValidCharacterReference', () => {
it( 'returns true for a named character reference', () => {
const result = isValidCharacterReference( 'blk12' );
expect( result ).toBe( true );
} );
it( 'returns true for a decimal character reference', () => {
const result = isValidCharacterReference( '#33' );
expect( result ).toBe( true );
} );
it( 'returns true for a hexadecimal character reference', () => {
const result = isValidCharacterReference( '#xC6' );
expect( result ).toBe( true );
} );
it( 'returns false for an invalid character reference', () => {
const result = isValidCharacterReference(
' Test</h2><h2>Test &'
);
expect( result ).toBe( false );
} );
} );
describe( 'DecodeEntityParser', () => {
it( 'can be constructed', () => {
expect(
new DecodeEntityParser() instanceof DecodeEntityParser
).toBe( true );
} );
it( 'returns parse as decoded value', () => {
expect( new DecodeEntityParser().parse( 'quot' ) ).toBe( '"' );
} );
} );
describe( 'getTextPiecesSplitOnWhitespace()', () => {
it( 'returns text pieces spilt on whitespace', () => {
const pieces = getTextPiecesSplitOnWhitespace( ' a \t b \n c' );
expect( pieces ).toEqual( [ 'a', 'b', 'c' ] );
} );
} );
describe( 'getTextWithCollapsedWhitespace()', () => {
it( 'returns text with collapsed whitespace', () => {
const pieces = getTextWithCollapsedWhitespace( ' a \t b \n c' );
expect( pieces ).toBe( 'a b c' );
} );
} );
describe( 'getMeaningfulAttributePairs()', () => {
it( 'returns with non-empty attributes', () => {
const pairs = getMeaningfulAttributePairs( {
attributes: [ [ 'class', 'a' ] ],
} );
expect( pairs ).toEqual( [ [ 'class', 'a' ] ] );
} );
it( 'returns without empty non-boolean, non-enumerated attributes', () => {
const pairs = getMeaningfulAttributePairs( {
attributes: [ [ 'class', '' ] ],
} );
expect( pairs ).toEqual( [] );
} );
it( 'returns with empty boolean attributes', () => {
const pairs = getMeaningfulAttributePairs( {
attributes: [ [ 'disabled', '' ] ],
} );
expect( pairs ).toEqual( [ [ 'disabled', '' ] ] );
} );
it( 'returns with empty enumerated attributes', () => {
const pairs = getMeaningfulAttributePairs( {
attributes: [ [ 'contenteditable', '' ] ],
} );
expect( pairs ).toEqual( [ [ 'contenteditable', '' ] ] );
} );
it( 'returns with empty data- attributes', () => {
const pairs = getMeaningfulAttributePairs( {
attributes: [ [ 'data-foo', '' ] ],
} );
expect( pairs ).toEqual( [ [ 'data-foo', '' ] ] );
} );
} );
describe( 'isEquivalentTextTokens()', () => {
it( 'should return false if not equal with collapsed whitespace', () => {
const isEqual = isEquivalentTextTokens(
{ chars: ' a \t b \n c' },
{ chars: 'a \n c \t b ' }
);
expect( console ).toHaveWarned();
expect( isEqual ).toBe( false );
} );
it( 'should return true if equal with collapsed whitespace', () => {
const isEqual = isEquivalentTextTokens(
{ chars: ' a \t b \n c' },
{ chars: 'a \n b \t c ' }
);
expect( isEqual ).toBe( true );
} );
} );
describe( 'getNormalizedLength()', () => {
it( 'omits unit from zero px length', () => {
const normalizedLength = getNormalizedLength( '0px' );
expect( normalizedLength ).toBe( '0' );
} );
it( 'retains unit in non-zero px length', () => {
const normalizedLength = getNormalizedLength( '50px' );
expect( normalizedLength ).toBe( '50px' );
} );
it( 'omits unit from zero percentage', () => {
const normalizedLength = getNormalizedLength( '0%' );
expect( normalizedLength ).toBe( '0' );
} );
it( 'retains unit in non-zero percentage', () => {
const normalizedLength = getNormalizedLength( '50%' );
expect( normalizedLength ).toBe( '50%' );
} );
it( 'adds leading zero to percentage', () => {
const normalizedLength = getNormalizedLength( '.5%' );
expect( normalizedLength ).toBe( '0.5%' );
} );
} );
describe( 'getNormalizedStyleValue()', () => {
it( 'omits whitespace and quotes from url value', () => {
const normalizedValue = getNormalizedStyleValue(
'url( "https://wordpress.org/img.png" )'
);
expect( normalizedValue ).toBe(
'url(https://wordpress.org/img.png)'
);
} );
it( 'omits length units from zero values', () => {
const normalizedValue =
getNormalizedStyleValue( '44% 0% 18em 0em' );
expect( normalizedValue ).toBe( '44% 0 18em 0' );
} );
it( 'add leading zero to units that have it missing', () => {
const normalizedValue = getNormalizedStyleValue( '.23% .75em' );
expect( normalizedValue ).toBe( '0.23% 0.75em' );
} );
it( 'leaves zero values in calc() expressions alone', () => {
const normalizedValue =
getNormalizedStyleValue( 'calc(0em + 5px)' );
expect( normalizedValue ).toBe( 'calc(0em + 5px)' );
} );
} );
describe( 'getStyleProperties()', () => {
it( 'returns style property pairs', () => {
const pairs = getStyleProperties(
'background-image: url( "https://wordpress.org/img.png" ); color: red;'
);
expect( pairs ).toEqual( {
'background-image': 'url(https://wordpress.org/img.png)',
color: 'red',
} );
} );
} );
describe( 'isEqualAttributesOfName', () => {
describe( '.class()', () => {
it( 'ignores ordering', () => {
const isEqual = isEqualAttributesOfName.class(
'a b c',
'b a c'
);
expect( isEqual ).toBe( true );
} );
it( 'ignores whitespace', () => {
const isEqual = isEqualAttributesOfName.class(
'a b c',
'b a c'
);
expect( isEqual ).toBe( true );
} );
it( 'returns false if not equal', () => {
const isEqual = isEqualAttributesOfName.class(
'a b c',
'b a c d'
);
expect( isEqual ).toBe( false );
} );
} );
describe( '.style()', () => {
it( 'returns true if the same style', () => {
const isEqual = isEqualAttributesOfName.style(
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
"color: red; background-image: url('https://wordpress.org/img.png\n);"
);
expect( isEqual ).toBe( true );
} );
it( 'returns false if not same style', () => {
const isEqual = isEqualAttributesOfName.style(
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
"color: red; font-size: 13px; background-image: url('https://wordpress.org/img.png');"
);
expect( isEqual ).toBe( false );
} );
} );
describe( 'boolean attributes', () => {
it( 'returns true if both present', () => {
const isEqual = isEqualAttributesOfName.controls( 'true', '' );
expect( isEqual ).toBe( true );
} );
} );
} );
describe( 'isEqualTagAttributePairs()', () => {
it( 'returns false if not equal pairs', () => {
const isEqual = isEqualTagAttributePairs(
[ [ 'class', 'b a c' ] ],
[
[ 'class', 'c a b' ],
[
'style',
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
],
]
);
expect( console ).toHaveWarned();
expect( isEqual ).toBe( false );
} );
it( 'returns true if equal pairs', () => {
const isEqual = isEqualTagAttributePairs(
[
[ 'class', 'b a c' ],
[
'style',
'color: red; background-image: url( "https://wordpress.org/img.png" );',
],
[ 'controls', '' ],
],
[
[ 'class', 'c a b' ],
[
'style',
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
],
[ 'controls', 'true' ],
]
);
expect( isEqual ).toBe( true );
} );
it( 'returns true if case-insensitive equal pairs', () => {
const isEqual = isEqualTagAttributePairs(
[
[ 'ID', 'foo' ],
[ 'class', 'a b' ],
[ 'Style', 'color: red;' ],
],
[
[ 'id', 'foo' ],
[ 'CLASS', 'a b' ],
[ 'style', 'color: red;' ],
]
);
expect( isEqual ).toBe( true );
} );
} );
describe( 'isEqualTokensOfType', () => {
describe( '.StartTag()', () => {
it( 'returns false if tag name not the same', () => {
const isEqual = isEqualTokensOfType.StartTag(
{ tagName: 'p' },
{ tagName: 'section' }
);
expect( console ).toHaveWarned();
expect( isEqual ).toBe( false );
} );
it( 'returns false if tag name the same but attributes not', () => {
const isEqual = isEqualTokensOfType.StartTag(
{
tagName: 'p',
attributes: [ [ 'class', 'b a c' ] ],
},
{
tagName: 'p',
attributes: [
[ 'class', 'c a b' ],
[
'style',
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
],
],
}
);
expect( console ).toHaveWarned();
expect( isEqual ).toBe( false );
} );
it( 'returns true if tag name the same and attributes the same', () => {
const isEqual = isEqualTokensOfType.StartTag(
{
tagName: 'p',
attributes: [
[ 'class', 'b a c' ],
[
'style',
'color: red; background-image: url( "https://wordpress.org/img.png" );',
],
],
},
{
tagName: 'p',
attributes: [
[ 'class', 'c a b' ],
[
'style',
'background-image: url( "https://wordpress.org/img.png" ); color: red;',
],
],
}
);
expect( isEqual ).toBe( true );
} );
it( 'returns true if tag and attributes names are case insensitive the same', () => {
const isEqual = isEqualTokensOfType.StartTag(
{
tagName: 'P',
attributes: [
[ 'CLASS', 'a b' ],
[ 'style', 'color: red;' ],
],
},
{
tagName: 'p',
attributes: [
[ 'class', 'a b' ],
[ 'Style', 'color: red;' ],
],
}
);
expect( isEqual ).toBe( true );
} );
} );
} );
describe( 'getNextNonWhitespaceToken()', () => {
it( 'returns the next non-whitespace token, mutating array', () => {
const tokens = [
{ type: 'Chars', chars: ' \n\t' },
{ type: 'StartTag', tagName: 'p' },
];
const token = getNextNonWhitespaceToken( tokens );
expect( token ).toEqual( { type: 'StartTag', tagName: 'p' } );
expect( tokens ).toHaveLength( 0 );
} );
it( 'returns undefined if token options exhausted', () => {
const tokens = [ { type: 'Chars', chars: ' \n\t' } ];
const token = getNextNonWhitespaceToken( tokens );
expect( token ).toBeUndefined();
expect( tokens ).toHaveLength( 0 );
} );
} );
describe( 'isClosedByToken()', () => {
it( 'should return true if self-closed token is closed by an end token', () => {
const isClosed = isClosedByToken(
{ type: 'StartTag', tagName: 'div', selfClosing: true },
{ type: 'EndTag', tagName: 'div' }
);
expect( isClosed ).toBe( true );
} );
it( 'should return false if open token is not closed by an end token', () => {
const isClosed = isClosedByToken(
{ type: 'StartTag', tagName: 'div', selfClosing: false },
{ type: 'EndTag', tagName: 'div' }
);
expect( isClosed ).toBe( false );
} );
it( 'should return false if self-closed token has a different name to the end token', () => {
const isClosed = isClosedByToken(
{ type: 'StartTag', tagName: 'div', selfClosing: true },
{ type: 'EndTag', tagName: 'span' }
);
expect( isClosed ).toBe( false );
} );
it( 'should return false if self-closed token is not closed by a start token', () => {
const isClosed = isClosedByToken(
{ type: 'StartTag', tagName: 'div', selfClosing: true },
{ type: 'StartTag', tagName: 'div' }
);
expect( isClosed ).toBe( false );
} );
it( 'should return false if self-closed token is not closed by an undefined token', () => {
const isClosed = isClosedByToken(
{ type: 'StartTag', tagName: 'div', selfClosing: true },
undefined
);
expect( isClosed ).toBe( false );
} );
} );
describe( 'isEquivalentHTML()', () => {
it( 'should return true for identical markup', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello <span class="b">World!</span></div>',
'<div>Hello <span class="b">World!</span></div>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return false for effectively inequivalent html', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello <span class="b">World!</span></div>',
'<div>Hello <span class="a">World!</span></div>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return true for effectively equivalent html', () => {
const isEquivalent = isEquivalentHTML(
'<div>" Hello<SPAN class="b a" ID="foo" data-foo="here — there"> World! 😅</ SPAN> "</div>',
'<div >" Hello\n<span id="foo" class="a b" data-foo="here — there">World! 😅</span>"</div>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should account for character reference validity', () => {
// Regression: Previously the validator would wrongly evaluate the
// segment of text ` Test</h2><h2>Test &` as a character
// reference, as it's between an opening `&` and terminating `;`.
//
// See: https://github.com/WordPress/gutenberg/issues/12448
const isEquivalent = isEquivalentHTML(
'<h2>Test & Test</h2><h2>Test & Test</h2>',
'<h2>Test & Test</h2><h2>Test & Test</h2>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return false when more tokens in first', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello</div>',
'<div>Hello'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return false when more tokens in second', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello',
'<div>Hello</div>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return true when first has trailing whitespace', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello</div> ',
'<div>Hello</div>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return true when second has trailing whitespace', () => {
const isEquivalent = isEquivalentHTML(
'<div>Hello</div>',
'<div>Hello</div> '
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return true when difference of empty non-boolean, non-enumerated attribute', () => {
const isEquivalent = isEquivalentHTML(
'<div class="">Hello</div>',
'<div>Hello</div>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return false when difference of empty boolean attribute', () => {
const isEquivalent = isEquivalentHTML(
'<input disabled>',
'<input>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return false when difference of empty enumerated attribute', () => {
const isEquivalent = isEquivalentHTML(
'<div contenteditable>',
'<div>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return false when difference of data- attribute', () => {
const isEquivalent = isEquivalentHTML( '<div data-foo>', '<div>' );
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return false when difference of boolean attribute', () => {
const isEquivalent = isEquivalentHTML(
'<video controls></video>',
'<video></video>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return true when same boolean attribute', () => {
const isEquivalent = isEquivalentHTML(
'<video controls></video>',
'<video controls></video>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return true when effectively same boolean attribute', () => {
const isEquivalent = isEquivalentHTML(
'<video controls></video>',
'<video controls=""></video>'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return true when comparing empty strings', () => {
const isEquivalent = isEquivalentHTML( '', '' );
expect( isEquivalent ).toBe( true );
} );
it( 'should return false if supplied malformed HTML', () => {
const isEquivalent = isEquivalentHTML(
'<blockquote class="wp-block-quote">fsdfsdfsd<p>fdsfsdfsdd</pfd fd fd></blockquote>',
'<blockquote class="wp-block-quote">fsdfsdfsd<p>fdsfsdfsdd</p></blockquote>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return false if supplied two sets of malformed HTML', () => {
const isEquivalent = isEquivalentHTML(
'<div>fsdfsdfsd<p>fdsfsdfsdd</pfd fd fd></div>',
'<blockquote>fsdfsdfsd<p>fdsfsdfsdd</p a></blockquote>'
);
expect( console ).toHaveWarned();
expect( isEquivalent ).toBe( false );
} );
it( 'should return true when comparing self-closing and normal tags', () => {
let isEquivalent = isEquivalentHTML(
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none" />',
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"></path>'
);
expect( isEquivalent ).toBe( true );
isEquivalent = isEquivalentHTML(
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"></path>',
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none" />'
);
expect( isEquivalent ).toBe( true );
} );
it( 'should return true when comparing self-closing and normal tags, ignoring trailing space', () => {
let isEquivalent = isEquivalentHTML(
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"/>',
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"></path>'
);
expect( isEquivalent ).toBe( true );
isEquivalent = isEquivalentHTML(
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"></path>',
'<path d="M0,0h24v24H0V0z M0,0h24v24H0V0z" fill="none"/>'
);
expect( isEquivalent ).toBe( true );
} );
} );
describe( 'validateBlock()', () => {
it( 'returns false if block is not valid', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
const [ isValid ] = validateBlock( {
name: 'core/test-block',
attrs: {
fruit: 'Bananas',
},
originalContent: 'Apples',
} );
expect( isValid ).toBe( false );
} );
it( 'returns false if error occurs while generating block save', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
save() {
throw new Error();
},
} );
const [ isValid ] = validateBlock( {
name: 'core/test-block',
attrs: {
fruit: 'Bananas',
},
originalContent: 'Bananas',
} );
expect( isValid ).toBe( false );
} );
it( 'returns true is block is valid', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
const [ isValid ] = validateBlock( {
name: 'core/test-block',
attributes: { fruit: 'Bananas' },
originalContent: 'Bananas',
} );
expect( isValid ).toBe( true );
} );
} );
describe( 'createLogger()', () => {
it( 'creates logger that pre-processes string substitutions', () => {
createLogger().warning( '%o', { foo: 'bar' } );
expect( console ).toHaveWarnedWith( "{ foo: 'bar' }" );
} );
} );
} );