@wordpress/blocks
Version:
Block API for WordPress.
1,796 lines (1,662 loc) • 47.8 kB
JavaScript
/**
* WordPress dependencies
*/
import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks';
import { logged } from '@wordpress/deprecated';
import { select, dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import {
registerBlockType,
registerBlockCollection,
registerBlockVariation,
registerBlockBindingsSource,
unregisterBlockCollection,
unregisterBlockType,
unregisterBlockBindingsSource,
setFreeformContentHandlerName,
getFreeformContentHandlerName,
setUnregisteredTypeHandlerName,
getUnregisteredTypeHandlerName,
setDefaultBlockName,
getDefaultBlockName,
getGroupingBlockName,
setGroupingBlockName,
getBlockType,
getBlockTypes,
getBlockSupport,
getBlockVariations,
getBlockBindingsSource,
hasBlockSupport,
isReusableBlock,
unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase
} from '../registration';
import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../constants';
import { omit } from '../utils';
import { store as blocksStore } from '../../store';
import { unlock } from '../../lock-unlock';
import { logged as warningLoggedSet } from '../../../../warning/src/utils';
const noop = () => {};
const pick = ( obj, keys ) =>
Object.fromEntries(
Object.entries( obj ).filter( ( [ key ] ) => keys.includes( key ) )
);
describe( 'blocks', () => {
const defaultBlockSettings = {
save: noop,
category: 'text',
title: 'block title',
};
afterEach( () => {
const registeredNames = Object.keys(
unlock( select( blocksStore ) ).getUnprocessedBlockTypes()
);
dispatch( blocksStore ).removeBlockTypes( registeredNames );
setFreeformContentHandlerName( undefined );
setUnregisteredTypeHandlerName( undefined );
setDefaultBlockName( undefined );
// Reset deprecation logging to ensure we properly track warnings.
for ( const key in logged ) {
delete logged[ key ];
}
warningLoggedSet.clear();
} );
describe( 'registerBlockType()', () => {
it( 'should reject numbers', () => {
const block = registerBlockType( 999 );
expect( console ).toHaveWarnedWith(
'Block names must be strings.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks without a namespace', () => {
const block = registerBlockType( 'doing-it-wrong' );
expect( console ).toHaveWarnedWith(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with too many namespaces', () => {
const block = registerBlockType( 'doing/it/wrong' );
expect( console ).toHaveWarnedWith(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with invalid characters', () => {
const block = registerBlockType( 'still/_doing_it_wrong' );
expect( console ).toHaveWarnedWith(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
expect( block ).toBeUndefined();
} );
it( 'Should reject blocks with the block name itself as the only parent attribute value', () => {
const block = registerBlockType( 'core/test-block', {
...defaultBlockSettings,
parent: [ 'core/test-block' ],
} );
expect( console ).toHaveWarnedWith(
'Block "core/test-block" cannot be a parent of itself. Please remove the block name from the parent list.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with uppercase characters', () => {
const block = registerBlockType( 'Core/Paragraph' );
expect( console ).toHaveWarnedWith(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks not starting with a letter', () => {
const block = registerBlockType(
'my-plugin/4-fancy-block',
defaultBlockSettings
);
expect( console ).toHaveWarnedWith(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
expect( block ).toBeUndefined();
} );
it( 'should accept valid block names', () => {
const block = registerBlockType(
'my-plugin/fancy-block-4',
defaultBlockSettings
);
expect( console ).not.toHaveWarned();
expect( block ).toEqual( {
apiVersion: 1,
name: 'my-plugin/fancy-block-4',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
save: noop,
category: 'text',
title: 'block title',
} );
} );
it( 'should prohibit registering the same block twice', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
const block = registerBlockType(
'core/test-block',
defaultBlockSettings
);
expect( console ).toHaveWarnedWith(
'Block "core/test-block" is already registered.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with invalid save function', () => {
const block = registerBlockType( 'my-plugin/fancy-block-5', {
...defaultBlockSettings,
attributes: {},
keywords: [],
save: 'invalid',
} );
expect( console ).toHaveWarnedWith(
'The "save" property must be a valid function.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with an invalid edit function', () => {
const blockType = {
save: noop,
edit: {},
category: 'text',
title: 'block title',
},
block = registerBlockType(
'my-plugin/fancy-block-6',
blockType
);
expect( console ).toHaveWarnedWith(
'The "edit" property must be a valid component.'
);
expect( block ).toBeUndefined();
} );
it( 'should canonicalize legacy block category.', () => {
const blockType = {
save: noop,
category: 'common',
title: 'block title',
},
block = registerBlockType(
'my-plugin/fancy-block-9',
blockType
);
expect( block.category ).toBe( 'text' );
} );
it( 'should unset category of blocks with non registered category.', () => {
const blockType = {
save: noop,
category: 'custom-category-slug',
title: 'block title',
},
block = registerBlockType(
'my-plugin/fancy-block-9',
blockType
);
expect( console ).toHaveWarnedWith(
'The block "my-plugin/fancy-block-9" is registered with an invalid category "custom-category-slug".'
);
expect( block ).not.toBeUndefined();
expect( block.category ).toBeUndefined();
} );
it( 'should reject blocks without title', () => {
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
},
block = registerBlockType(
'my-plugin/fancy-block-9',
blockType
);
expect( console ).toHaveWarnedWith(
'The block "my-plugin/fancy-block-9" must have a title.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks with empty titles', () => {
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: '',
},
block = registerBlockType(
'my-plugin/fancy-block-10',
blockType
);
expect( console ).toHaveWarnedWith(
'The block "my-plugin/fancy-block-10" must have a title.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject titles which are not strings', () => {
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: 12345,
},
block = registerBlockType(
'my-plugin/fancy-block-11',
blockType
);
expect( console ).toHaveWarnedWith(
'Block titles must be strings.'
);
expect( block ).toBeUndefined();
} );
it( 'should assign default settings', () => {
registerBlockType( 'core/test-block-with-defaults', {
title: 'block title',
category: 'text',
} );
expect( getBlockType( 'core/test-block-with-defaults' ) ).toEqual( {
apiVersion: 1,
name: 'core/test-block-with-defaults',
title: 'block title',
category: 'text',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
save: expect.any( Function ),
} );
} );
it( 'should default to empty object when attributes is omitted and without warning', () => {
registerBlockType( 'core/test-block-omitted-attributes', {
title: 'block title',
category: 'text',
save: noop,
} );
// Verify no warning was shown (unlike when explicitly set to null/undefined)
expect( console ).not.toHaveWarned();
const blockType = getBlockType(
'core/test-block-omitted-attributes'
);
expect( blockType.attributes ).toEqual( {} );
} );
it.each( [
[ 'undefined', undefined ],
[ 'null', null ],
] )(
'should warn and default to empty object when attributes is %s',
( _label, value ) => {
registerBlockType( 'core/test-block-null-attributes', {
title: 'block title',
category: 'text',
save: noop,
attributes: value,
} );
expect( console ).toHaveWarnedWith(
'The block "core/test-block-null-attributes" is registering attributes as `null` or `undefined`. Use an empty object (`attributes: {}`) or exclude the `attributes` key.'
);
const blockType = getBlockType(
'core/test-block-null-attributes'
);
expect( blockType.attributes ).toEqual( {} );
}
);
it( 'should default to browser-initialized global attributes', () => {
const attributes = { ok: { type: 'boolean' } };
unstable__bootstrapServerSideBlockDefinitions( {
'core/test-block-with-attributes': { attributes },
} );
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
};
registerBlockType( 'core/test-block-with-attributes', blockType );
expect( getBlockType( 'core/test-block-with-attributes' ) ).toEqual(
{
apiVersion: 1,
name: 'core/test-block-with-attributes',
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {
ok: {
type: 'boolean',
},
},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
}
);
} );
it( 'should skip null values returned from the server', () => {
const blockName = 'core/test-block-with-null-server-values';
unstable__bootstrapServerSideBlockDefinitions( {
[ blockName ]: {
icon: null,
category: null,
parent: null,
attributes: null,
example: null,
},
} );
const blockType = {
title: 'block title',
};
registerBlockType( blockName, blockType );
expect( getBlockType( blockName ) ).toEqual( {
apiVersion: 1,
name: blockName,
save: expect.any( Function ),
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should map incompatible keys returned from the server', () => {
const blockName = 'core/test-block-with-incompatible-keys';
unstable__bootstrapServerSideBlockDefinitions( {
[ blockName ]: {
api_version: 3,
provides_context: {
fontSize: 'fontSize',
},
uses_context: [ 'textColor' ],
block_hooks: {
'tests/my-block': 'after',
},
},
} );
const blockType = {
title: 'block title',
};
registerBlockType( blockName, blockType );
expect( getBlockType( blockName ) ).toEqual( {
apiVersion: 3,
name: blockName,
save: expect.any( Function ),
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {
fontSize: 'fontSize',
},
usesContext: [ 'textColor' ],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {
'tests/my-block': 'after',
},
} );
} );
it( 'should merge settings provided by server and client', () => {
const blockName = 'core/test-block-with-merged-settings';
unstable__bootstrapServerSideBlockDefinitions( {
[ blockName ]: {
variations: [
{ name: 'foo', label: 'Foo' },
{ name: 'baz', label: 'Baz', description: 'Testing' },
],
},
} );
const blockType = {
title: 'block settings merge',
variations: [
{ name: 'bar', label: 'Bar' },
{ name: 'baz', label: 'Baz', icon: 'layout' },
],
};
registerBlockType( blockName, blockType );
expect( getBlockType( blockName ) ).toEqual( {
apiVersion: 1,
name: blockName,
save: expect.any( Function ),
title: 'block settings merge',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [
{ name: 'foo', label: 'Foo' },
{
description: 'Testing',
name: 'baz',
label: 'Baz',
icon: 'layout',
},
{ name: 'bar', label: 'Bar' },
],
blockHooks: {},
} );
} );
it( 'should validate the icon', () => {
const blockType = {
save: noop,
category: 'text',
title: 'block title',
icon: { chicken: 'ribs' },
};
const block = registerBlockType(
'core/test-block-icon-normalize-element',
blockType
);
expect( console ).toHaveWarned();
expect( block ).toBeUndefined();
} );
it( 'should normalize the icon containing an element', () => {
const blockType = {
save: noop,
category: 'text',
title: 'block title',
icon: (
<svg width="20" height="20" viewBox="0 0 20 20">
<circle
cx="10"
cy="10"
r="10"
fill="red"
stroke="blue"
strokeWidth="10"
/>
</svg>
),
};
registerBlockType(
'core/test-block-icon-normalize-element',
blockType
);
expect(
getBlockType( 'core/test-block-icon-normalize-element' )
).toEqual( {
apiVersion: 1,
name: 'core/test-block-icon-normalize-element',
save: noop,
category: 'text',
title: 'block title',
icon: {
src: (
<svg width="20" height="20" viewBox="0 0 20 20">
<circle
cx="10"
cy="10"
r="10"
fill="red"
stroke="blue"
strokeWidth="10"
/>
</svg>
),
},
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should normalize the icon containing a string', () => {
const blockType = {
save: noop,
category: 'text',
title: 'block title',
icon: 'foo',
};
registerBlockType(
'core/test-block-icon-normalize-string',
blockType
);
expect(
getBlockType( 'core/test-block-icon-normalize-string' )
).toEqual( {
apiVersion: 1,
name: 'core/test-block-icon-normalize-string',
save: noop,
category: 'text',
title: 'block title',
icon: {
src: 'foo',
},
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should normalize the icon containing a function', () => {
const MyTestIcon = () => {
return (
<svg width="20" height="20" viewBox="0 0 20 20">
<circle
cx="10"
cy="10"
r="10"
fill="red"
stroke="blue"
strokeWidth="10"
/>
</svg>
);
};
const blockType = {
save: noop,
category: 'text',
title: 'block title',
icon: MyTestIcon,
};
registerBlockType(
'core/test-block-icon-normalize-function',
blockType
);
expect(
getBlockType( 'core/test-block-icon-normalize-function' )
).toEqual( {
apiVersion: 1,
name: 'core/test-block-icon-normalize-function',
save: noop,
category: 'text',
title: 'block title',
icon: {
src: MyTestIcon,
},
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should correctly register an icon with background and a custom svg', () => {
const blockType = {
save: noop,
category: 'text',
title: 'block title',
icon: {
background: '#f00',
src: (
<svg width="20" height="20" viewBox="0 0 20 20">
<circle
cx="10"
cy="10"
r="10"
fill="red"
stroke="blue"
strokeWidth="10"
/>
</svg>
),
},
};
registerBlockType(
'core/test-block-icon-normalize-background',
blockType
);
expect(
getBlockType( 'core/test-block-icon-normalize-background' )
).toEqual( {
apiVersion: 1,
name: 'core/test-block-icon-normalize-background',
save: noop,
category: 'text',
title: 'block title',
icon: {
background: '#f00',
foreground: '#191e23',
shadowColor: 'rgba(255, 0, 0, 0.3)',
src: (
<svg width="20" height="20" viewBox="0 0 20 20">
<circle
cx="10"
cy="10"
r="10"
fill="red"
stroke="blue"
strokeWidth="10"
/>
</svg>
),
},
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should store a copy of block type', () => {
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
};
registerBlockType( 'core/test-block-with-settings', blockType );
blockType.mutated = true;
expect( getBlockType( 'core/test-block-with-settings' ) ).toEqual( {
apiVersion: 1,
name: 'core/test-block-with-settings',
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should transform parent string to array', () => {
const blockType = {
save: noop,
category: 'text',
title: 'block title',
parent: 'core/paragraph',
};
const block = registerBlockType(
'core/test-block-parent-string',
blockType
);
expect( console ).toHaveWarnedWith(
'Parent must be undefined or an array of strings (block types), but it is a string.'
);
expect( block ).toEqual( {
apiVersion: 1,
name: 'core/test-block-parent-string',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
parent: [ 'core/paragraph' ],
} );
} );
describe( 'applyFilters', () => {
afterEach( () => {
removeAllFilters( 'blocks.registerBlockType' );
} );
it( 'should reject valid blocks when they become invalid after executing filter', () => {
addFilter(
'blocks.registerBlockType',
'core/blocks/without-title',
( settings ) => {
return {
...settings,
title: '',
};
}
);
const block = registerBlockType(
'my-plugin/fancy-block-12',
defaultBlockSettings
);
expect( console ).toHaveWarnedWith(
'The block "my-plugin/fancy-block-12" must have a title.'
);
expect( block ).toBeUndefined();
} );
it( 'should reject blocks which become invalid after executing filter which does not return a plain object', () => {
addFilter(
'blocks.registerBlockType',
'core/blocks/without-save',
( settings ) => {
return [ settings ];
}
);
const block = registerBlockType(
'my-plugin/fancy-block-13',
defaultBlockSettings
);
expect( console ).toHaveWarnedWith(
'Block settings must be a valid object.'
);
expect( block ).toBeUndefined();
} );
it( 'should apply the blocks.registerBlockType filter to each of the deprecated settings as well as the main block settings', () => {
const name = 'my-plugin/fancy-block-13';
const blockSettingsWithDeprecations = {
...defaultBlockSettings,
deprecated: [
{
save() {
return 1;
},
},
{
save() {
return 2;
},
},
],
};
let i = 0;
addFilter(
'blocks.registerBlockType',
'core/blocks/without-title',
( settings ) => {
// Verify that for deprecations, the filter is called with a merge of pre-filter
// settings with deprecation keys omitted and the deprecation entry.
if ( i > 0 ) {
// eslint-disable-next-line jest/no-conditional-expect
expect( settings ).toEqual( {
...omit(
{
name,
icon: BLOCK_ICON_DEFAULT,
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
save: () => null,
...blockSettingsWithDeprecations,
},
DEPRECATED_ENTRY_KEYS
),
...blockSettingsWithDeprecations.deprecated[
i - 1
],
} );
}
i++;
return {
...settings,
attributes: {
...settings.attributes,
id: {
type: 'string',
},
},
};
}
);
const block = registerBlockType(
name,
blockSettingsWithDeprecations
);
expect( block.attributes.id ).toEqual( { type: 'string' } );
block.deprecated.forEach( ( deprecation ) => {
expect( deprecation.attributes.id ).toEqual( {
type: 'string',
} );
// Verify that the deprecation's keys are a subset of deprecation keys.
expect( deprecation ).toEqual(
pick( deprecation, DEPRECATED_ENTRY_KEYS )
);
} );
} );
it( 'should update block attributes separately for each block when they use a default set', () => {
addFilter(
'blocks.registerBlockType',
'core/blocks/shared-defaults',
( settings, name ) => {
if ( name === 'my-plugin/test-block-1' ) {
settings.attributes.newlyAddedAttribute = {
type: String,
};
}
return settings;
}
);
const block1 = registerBlockType(
'my-plugin/test-block-1',
defaultBlockSettings
);
const block2 = registerBlockType(
'my-plugin/test-block-2',
defaultBlockSettings
);
// Only attributes of block1 are supposed to be edited by the filter thus it must differ from block2.
expect( block1.attributes ).not.toEqual( block2.attributes );
} );
it( 'should allow non-string descriptions at registration but warn for undesired usage.', () => {
const newDescription = <p>foo bar</p>;
const block = registerBlockType( 'my-plugin/test-block-1', {
...defaultBlockSettings,
description: newDescription,
} );
expect( block.description ).toBe( newDescription );
expect( console ).toHaveWarnedWith(
'Declaring non-string block descriptions is deprecated since version 6.2.'
);
} );
it( 'should allow non-string descriptions through `blocks.registerBlockType` filter but warn for undesired usage.', () => {
const newDescription = <p>foo bar</p>;
addFilter(
'blocks.registerBlockType',
'core/blocks/non-string-description',
( settings ) => {
settings.description = newDescription;
return settings;
}
);
const block = registerBlockType(
'my-plugin/test-block-2',
defaultBlockSettings
);
expect( block.description ).toBe( newDescription );
expect( console ).toHaveWarnedWith(
'Declaring non-string block descriptions is deprecated since version 6.2.'
);
} );
it( 're-applies block filters', () => {
// register block
registerBlockType( 'test/block', defaultBlockSettings );
// register a filter after registering a block
addFilter(
'blocks.registerBlockType',
'core/blocks/reapply',
( settings ) => ( {
...settings,
title: settings.title + ' filtered',
} )
);
// check that block type has unfiltered values
expect( getBlockType( 'test/block' ).title ).toBe(
'block title'
);
// reapply the block filters
dispatch( blocksStore ).reapplyBlockTypeFilters();
// check that block type has filtered values
expect( getBlockType( 'test/block' ).title ).toBe(
'block title filtered'
);
} );
} );
test( 'registers block from metadata', () => {
const Edit = () => 'test';
const block = registerBlockType(
{
name: 'test/block-from-metadata',
title: 'Block from metadata',
category: 'text',
icon: 'palmtree',
variations: [
{
name: 'variation',
title: 'Variation Title',
description: 'Variation description',
keywords: [ 'variation' ],
},
],
},
{
edit: Edit,
save: noop,
}
);
expect( block ).toEqual( {
apiVersion: 1,
name: 'test/block-from-metadata',
title: 'Block from metadata',
category: 'text',
icon: {
src: 'palmtree',
},
keywords: [],
attributes: {},
providesContext: {},
usesContext: [],
selectors: {},
supports: {},
styles: [],
variations: [
{
name: 'variation',
title: 'Variation Title',
description: 'Variation description',
keywords: [ 'variation' ],
},
],
blockHooks: {},
edit: Edit,
save: noop,
} );
} );
test( 'registers block from metadata with translation', () => {
addFilter(
'i18n.gettext_with_context_test',
'test/mark-as-translated',
( value ) => value + ' (translated)'
);
const Edit = () => 'test';
const block = registerBlockType(
{
name: 'test/block-from-metadata-i18n',
title: 'I18n title from metadata',
description: 'I18n description from metadata',
keywords: [ 'i18n', 'metadata' ],
styles: [
{
name: 'i18n-style',
label: 'I18n Style Label',
},
],
variations: [
{
name: 'i18n-variation',
title: 'I18n Variation Title',
description: 'I18n variation description',
keywords: [ 'variation' ],
},
],
textdomain: 'test',
icon: 'palmtree',
},
{
edit: Edit,
save: noop,
}
);
removeFilter(
'i18n.gettext_with_context_test',
'test/mark-as-translated'
);
expect( block ).toEqual( {
apiVersion: 1,
name: 'test/block-from-metadata-i18n',
title: 'I18n title from metadata (translated)',
description: 'I18n description from metadata (translated)',
icon: {
src: 'palmtree',
},
keywords: [ 'i18n (translated)', 'metadata (translated)' ],
attributes: {},
providesContext: {},
usesContext: [],
selectors: {},
supports: {},
styles: [
{
name: 'i18n-style',
label: 'I18n Style Label (translated)',
},
],
variations: [
{
name: 'i18n-variation',
title: 'I18n Variation Title (translated)',
description: 'I18n variation description (translated)',
keywords: [ 'variation (translated)' ],
},
],
blockHooks: {},
edit: Edit,
save: noop,
} );
} );
} );
describe( 'registerBlockCollection()', () => {
it( 'creates a new block collection', () => {
registerBlockCollection( 'core', { title: 'Core' } );
expect( select( blocksStore ).getCollections() ).toEqual( {
core: { title: 'Core', icon: undefined },
} );
} );
} );
describe( 'unregisterBlockCollection()', () => {
it( 'removes a block collection', () => {
registerBlockCollection( 'core', { title: 'Core' } );
registerBlockCollection( 'core2', { title: 'Core2' } );
unregisterBlockCollection( 'core' );
expect( select( blocksStore ).getCollections() ).toEqual( {
core2: { title: 'Core2', icon: undefined },
} );
} );
} );
describe( 'unregisterBlockType()', () => {
it( 'should fail if a block is not registered', () => {
const oldBlock = unregisterBlockType( 'core/test-block' );
expect( console ).toHaveWarnedWith(
'Block "core/test-block" is not registered.'
);
expect( oldBlock ).toBeUndefined();
} );
it( 'should unregister existing blocks', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
expect( getBlockTypes() ).toEqual( [
{
apiVersion: 1,
name: 'core/test-block',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
},
] );
const oldBlock = unregisterBlockType( 'core/test-block' );
expect( console ).not.toHaveWarned();
expect( oldBlock ).toEqual( {
apiVersion: 1,
name: 'core/test-block',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
expect( getBlockTypes() ).toEqual( [] );
} );
} );
describe( 'setFreeformContentHandlerName()', () => {
it( 'assigns unknown type handler', () => {
setFreeformContentHandlerName( 'core/test-block' );
expect( getFreeformContentHandlerName() ).toBe( 'core/test-block' );
} );
} );
describe( 'getFreeformContentHandlerName()', () => {
it( 'defaults to undefined', () => {
expect( getFreeformContentHandlerName() ).toBeNull();
} );
} );
describe( 'setUnregisteredTypeHandlerName()', () => {
it( 'assigns unknown type handler', () => {
setUnregisteredTypeHandlerName( 'core/test-block' );
expect( getUnregisteredTypeHandlerName() ).toBe(
'core/test-block'
);
} );
} );
describe( 'getUnregisteredTypeHandlerName()', () => {
it( 'defaults to undefined', () => {
expect( getUnregisteredTypeHandlerName() ).toBeNull();
} );
} );
describe( 'setDefaultBlockName()', () => {
it( 'assigns default block name', () => {
setDefaultBlockName( 'core/test-block' );
expect( getDefaultBlockName() ).toBe( 'core/test-block' );
} );
} );
describe( 'getDefaultBlockName()', () => {
it( 'defaults to undefined', () => {
expect( getDefaultBlockName() ).toBeNull();
} );
} );
describe( 'getGroupingBlockName()', () => {
it( 'defaults to undefined', () => {
expect( getGroupingBlockName() ).toBeNull();
} );
} );
describe( 'setGroupingBlockName()', () => {
it( 'assigns default block name', () => {
setGroupingBlockName( 'core/test-block' );
expect( getGroupingBlockName() ).toBe( 'core/test-block' );
} );
} );
describe( 'getBlockType()', () => {
it( 'should return { name, save } for blocks with minimum settings', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
expect( getBlockType( 'core/test-block' ) ).toEqual( {
apiVersion: 1,
name: 'core/test-block',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
it( 'should return all block type elements', () => {
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
};
registerBlockType( 'core/test-block-with-settings', blockType );
expect( getBlockType( 'core/test-block-with-settings' ) ).toEqual( {
apiVersion: 1,
name: 'core/test-block-with-settings',
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
} );
} );
} );
describe( 'getBlockTypes()', () => {
it( 'should return an empty array at first', () => {
expect( getBlockTypes() ).toEqual( [] );
} );
it( 'should return all registered blocks', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
const blockType = {
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
};
registerBlockType( 'core/test-block-with-settings', blockType );
expect( getBlockTypes() ).toEqual( [
{
apiVersion: 1,
name: 'core/test-block',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
},
{
apiVersion: 1,
name: 'core/test-block-with-settings',
settingName: 'settingValue',
save: noop,
category: 'text',
title: 'block title',
icon: { src: BLOCK_ICON_DEFAULT },
attributes: {},
providesContext: {},
usesContext: [],
keywords: [],
selectors: {},
supports: {},
styles: [],
variations: [],
blockHooks: {},
},
] );
} );
} );
describe( 'getBlockSupport', () => {
it( 'should return undefined if block has no supports', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );
expect( getBlockSupport( 'core/test-block', 'foo' ) ).toBe(
undefined
);
} );
it( 'should return block supports value', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );
expect( getBlockSupport( 'core/test-block', 'bar' ) ).toBe( true );
} );
it( 'should return custom default supports if block does not define support by name', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );
expect( getBlockSupport( 'core/test-block', 'foo', true ) ).toBe(
true
);
} );
} );
describe( 'hasBlockSupport', () => {
it( 'should return false if block has no supports', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );
expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( false );
} );
it( 'should return false if block does not define support by name', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );
expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( false );
} );
it( 'should return custom default supports if block does not define support by name', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );
expect( hasBlockSupport( 'core/test-block', 'foo', true ) ).toBe(
true
);
} );
it( 'should return true if block type supports', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
foo: true,
},
} );
expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( true );
} );
it( 'should return true if block author defines unsupported but truthy value', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
foo: 'hmmm',
},
} );
expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( true );
} );
it( 'should handle block settings object as argument to test', () => {
const settings = {
...defaultBlockSettings,
supports: {
foo: true,
},
};
expect( hasBlockSupport( settings, 'foo' ) ).toBe( true );
} );
} );
describe( 'isReusableBlock', () => {
it( 'should return true for a reusable block', () => {
const block = { name: 'core/block' };
expect( isReusableBlock( block ) ).toBe( true );
} );
it( 'should return false for other blocks', () => {
const block = { name: 'core/paragraph' };
expect( isReusableBlock( block ) ).toBe( false );
} );
} );
describe( 'registerBlockVariation', () => {
it( 'should warn when registering block variation without a name', () => {
registerBlockType( 'core/variation-block', defaultBlockSettings );
registerBlockVariation( 'core/variation-block', {
title: 'Variation Title',
description: 'Variation description',
} );
expect( console ).toHaveWarnedWith(
'Variation names must be unique strings.'
);
expect( getBlockVariations( 'core/variation-block' ) ).toEqual( [
{
title: 'Variation Title',
description: 'Variation description',
},
] );
} );
} );
describe( 'registerBlockBindingsSource', () => {
// Check the name is correct.
it( 'should contain name property', () => {
const source = registerBlockBindingsSource( {} );
expect( console ).toHaveWarnedWith(
'Block bindings source must contain a name.'
);
expect( source ).toBeUndefined();
} );
it( 'should reject numbers', () => {
const source = registerBlockBindingsSource( { name: 1 } );
expect( console ).toHaveWarnedWith(
'Block bindings source name must be a string.'
);
expect( source ).toBeUndefined();
} );
it( 'should reject names with uppercase characters', () => {
registerBlockBindingsSource( {
name: 'Core/WrongName',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source name must not contain uppercase characters.'
);
expect(
getBlockBindingsSource( 'Core/WrongName' )
).toBeUndefined();
} );
it( 'should reject names with invalid characters', () => {
registerBlockBindingsSource( {
name: 'core/_wrong_name',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source name must contain only valid characters: lowercase characters, hyphens, or digits. Example: my-plugin/my-custom-source.'
);
expect(
getBlockBindingsSource( 'core/_wrong_name' )
).toBeUndefined();
} );
it( 'should reject invalid names without namespace', () => {
registerBlockBindingsSource( {
name: 'wrong-name',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source name must contain a namespace and valid characters. Example: my-plugin/my-custom-source.'
);
expect( getBlockBindingsSource( 'wrong-name' ) ).toBeUndefined();
} );
// Check the label is correct.
it( 'should contain label property', () => {
registerBlockBindingsSource( {
name: 'core/testing',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source must contain a label.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
it( 'should reject invalid label', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 1,
} );
expect( console ).toHaveWarnedWith(
'Block bindings source label must be a string.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
it( 'should override label from the server', () => {
// Simulate bootstrap source from the server.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'Server label',
} );
// Override the source with a different label in the client.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'Client label',
} );
expect( console ).toHaveWarnedWith(
'Block bindings "core/testing" source label was overridden.'
);
const source = getBlockBindingsSource( 'core/testing' );
unregisterBlockBindingsSource( 'core/testing' );
expect( source.label ).toEqual( 'Client label' );
} );
it( 'should keep label from the server when not defined in the client', () => {
// Simulate bootstrap source from the server.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'Server label',
} );
// Override the source with a different label in the client.
registerBlockBindingsSource( {
name: 'core/testing',
} );
const source = getBlockBindingsSource( 'core/testing' );
unregisterBlockBindingsSource( 'core/testing' );
expect( source.label ).toEqual( 'Server label' );
} );
// Check the `usesContext` array is correct.
it( 'should reject invalid usesContext property', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
usesContext: 'should be an array',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source usesContext must be an array.'
);
} );
it( 'should add usesContext when only defined in the server', () => {
// Simulate bootstrap source from the server.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
usesContext: [ 'postId', 'postType' ],
} );
// Register source in the client without usesContext.
registerBlockBindingsSource( {
name: 'core/testing',
getValue: () => 'value',
} );
const source = getBlockBindingsSource( 'core/testing' );
unregisterBlockBindingsSource( 'core/testing' );
expect( source.usesContext ).toEqual( [ 'postId', 'postType' ] );
} );
it( 'should add usesContext when only defined in the client', () => {
// Simulate bootstrap source from the server.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
} );
// Register source in the client with usesContext.
registerBlockBindingsSource( {
name: 'core/testing',
usesContext: [ 'postId', 'postType' ],
getValue: () => 'value',
} );
const source = getBlockBindingsSource( 'core/testing' );
unregisterBlockBindingsSource( 'core/testing' );
expect( source.usesContext ).toEqual( [ 'postId', 'postType' ] );
} );
it( 'should merge usesContext from server and client without duplicates', () => {
// Simulate bootstrap source from the server.
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
usesContext: [ 'postId', 'postType' ],
} );
// Register source in the client with usesContext.
registerBlockBindingsSource( {
name: 'core/testing',
usesContext: [ 'postType', 'clientContext' ],
getValue: () => 'value',
} );
const source = getBlockBindingsSource( 'core/testing' );
unregisterBlockBindingsSource( 'core/testing' );
expect( source.usesContext ).toEqual( [
'postId',
'postType',
'clientContext',
] );
} );
// Check the `getValues` callback is correct.
it( 'should reject invalid getValues callback', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
getValues: 'should be a function',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source getValues must be a function.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
// Check the `setValues` callback is correct.
it( 'should reject invalid setValues callback', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
setValues: 'should be a function',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source setValues must be a function.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
// Check the `canUserEditValue` callback is correct.
it( 'should reject invalid canUserEditValue callback', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
canUserEditValue: 'should be a function',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source canUserEditValue must be a function.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
// Check the `getFieldsList` callback is correct.
it( 'should reject invalid getFieldsList callback', () => {
registerBlockBindingsSource( {
name: 'core/testing',
label: 'testing',
getFieldsList: 'should be a function',
} );
expect( console ).toHaveWarnedWith(
'Block bindings source getFieldsList must be a function.'
);
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
// Check correct sources are registered as expected.
it( 'should register a valid source', () => {
const sourceProperties = {
label: 'Valid Source',
usesContext: [ 'postId' ],
getValues: () => 'value',
setValues: () => 'new values',
canUserEditValue: () => true,
};
registerBlockBindingsSource( {
name: 'core/valid-source',
...sourceProperties,
} );
expect( getBlockBindingsSource( 'core/valid-source' ) ).toEqual(
sourceProperties
);
unregisterBlockBindingsSource( 'core/valid-source' );
} );
it( 'should register a source with default values', () => {
registerBlockBindingsSource( {
name: 'core/valid-source',
label: 'Valid Source',
} );
const source = getBlockBindingsSource( 'core/valid-source' );
expect( source.usesContext ).toBeUndefined();
expect( source.getValues ).toBeUndefined();
expect( source.setValues ).toBeUndefined();
expect( source.canUserEditValue ).toBeUndefined();
expect( source.getFieldsList ).toBeUndefined();
unregisterBlockBindingsSource( 'core/valid-source' );
} );
it( 'should reject registering the same source twice', () => {
const source = {
name: 'core/test-source',
label: 'Test Source',
getValues: () => 'value',
};
registerBlockBindingsSource( source );
registerBlockBindingsSource( source );
unregisterBlockBindingsSource( 'core/test-source' );
expect( console ).toHaveWarnedWith(
'Block bindings source "core/test-source" is already registered.'
);
} );
} );
describe( 'unregisterBlockBindingsSource', () => {
it( 'should remove an existing block bindings source', () => {
registerBlockBindingsSource( {
name: 'core/test-source',
label: 'Test Source',
} );
unregisterBlockBindingsSource( 'core/test-source' );
expect(
getBlockBindingsSource( 'core/test-source' )
).toBeUndefined();
} );
it( 'should reject removing a source that does not exist', () => {
unregisterBlockBindingsSource( 'core/non-existing-source' );
expect( console ).toHaveWarnedWith(
'Block bindings source "core/non-existing-source" is not registered.'
);
} );
} );
} );