@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
2,459 lines (2,189 loc) • 58.2 kB
JavaScript
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* WordPress dependencies
*/
import {
registerBlockType,
unregisterBlockType,
createBlock,
getDefaultBlockName,
setDefaultBlockName,
setFreeformContentHandlerName,
getBlockTypes,
} from '@wordpress/blocks';
import { RawHTML } from '@wordpress/element';
/**
* Internal dependencies
*/
import * as _selectors from '../selectors';
const selectors = { ..._selectors };
const selectorNames = Object.keys( selectors );
selectorNames.forEach( ( name ) => {
selectors[ name ] = ( state, ...args ) => {
const select = () => ( {
getRawEntityRecord() {
return state.currentPost;
},
__experimentalGetDirtyEntityRecords() {
return (
state.__experimentalGetDirtyEntityRecords &&
state.__experimentalGetDirtyEntityRecords()
);
},
__experimentalGetEntitiesBeingSaved() {
return (
state.__experimentalGetEntitiesBeingSaved &&
state.__experimentalGetEntitiesBeingSaved()
);
},
getEntityRecordEdits() {
const present = state.editor && state.editor.present;
let edits = present && present.edits;
if ( state.initialEdits ) {
edits = {
...state.initialEdits,
...edits,
};
}
const { value: blocks, isDirty } =
( present && present.blocks ) || {};
if ( blocks && isDirty !== false ) {
edits = {
...edits,
blocks,
};
}
return edits;
},
hasEditsForEntityRecord() {
return Object.keys( this.getEntityRecordEdits() ).length > 0;
},
getEditedEntityRecord() {
return {
...this.getRawEntityRecord(),
...this.getEntityRecordEdits(),
};
},
getLastEntitySaveError() {
const saving = state.saving;
const successful = saving && saving.successful;
const error = saving && saving.error;
return successful === undefined ? error : ! successful;
},
hasUndo() {
return Boolean(
state.editor &&
state.editor.past &&
state.editor.past.length
);
},
hasRedo() {
return Boolean(
state.editor &&
state.editor.future &&
state.editor.future.length
);
},
getCurrentUser() {
return state.getCurrentUser && state.getCurrentUser();
},
hasFetchedAutosaves() {
return state.hasFetchedAutosaves && state.hasFetchedAutosaves();
},
getAutosave() {
return state.getAutosave && state.getAutosave();
},
getPostType() {
const postTypeLabel = {
post: 'Post',
page: 'Page',
}[ state.postType ];
return {
labels: {
singular_name: postTypeLabel,
},
supports: {
autosave: state.postType !== 'without-autosave',
},
};
},
getBlocks() {
return state.getBlocks && state.getBlocks();
},
} );
selectorNames.forEach( ( otherName ) => {
if ( _selectors[ otherName ].isRegistrySelector ) {
_selectors[ otherName ].registry = { select };
}
} );
return _selectors[ name ]( state, ...args );
};
selectors[ name ].isRegistrySelector =
_selectors[ name ].isRegistrySelector;
if ( selectors[ name ].isRegistrySelector ) {
selectors[ name ].registry = {
select: () => _selectors[ name ].registry.select(),
};
}
} );
const {
hasEditorUndo,
hasEditorRedo,
isEditedPostNew,
isEditedPostDirty,
hasNonPostEntityChanges,
isCleanNewPost,
getCurrentPost,
getCurrentPostId,
getCurrentPostLastRevisionId,
getCurrentPostRevisionsCount,
getCurrentPostType,
getPostEdits,
getEditedPostVisibility,
isCurrentPostPending,
isCurrentPostPublished,
isCurrentPostScheduled,
isEditedPostPublishable,
isEditedPostSaveable,
isEditedPostAutosaveable,
isEditedPostEmpty,
isEditedPostBeingScheduled,
isEditedPostDateFloating,
getCurrentPostAttribute,
getEditedPostAttribute,
isSavingPost,
isSavingNonPostEntityChanges,
didPostSaveRequestSucceed,
didPostSaveRequestFail,
getSuggestedPostFormat,
getEditedPostContent,
isPermalinkEditable,
getPermalink,
getPermalinkParts,
getEditedPostSlug,
isPostSavingLocked,
isPostAutosavingLocked,
canUserUseUnfilteredHTML,
getPostTypeLabel,
isEditorPanelRemoved,
isInserterOpened,
isListViewOpened,
} = selectors;
describe( 'selectors', () => {
let cachedSelectors;
beforeAll( () => {
cachedSelectors = Object.entries( selectors )
.filter( ( [ , selector ] ) => selector.clear )
.map( ( [ , selector ] ) => selector );
} );
beforeEach( () => {
registerBlockType( 'core/block', {
apiVersion: 3,
save: () => null,
category: 'reusable',
title: 'Reusable Block Stub',
supports: {
inserter: false,
},
} );
registerBlockType( 'core/test-block-a', {
apiVersion: 3,
save: ( props ) => props.attributes.text,
category: 'design',
title: 'Test Block A',
icon: 'test',
keywords: [ 'testing' ],
} );
registerBlockType( 'core/test-block-b', {
apiVersion: 3,
save: ( props ) => props.attributes.text,
category: 'text',
title: 'Test Block B',
icon: 'test',
keywords: [ 'testing' ],
supports: {
multiple: false,
},
} );
registerBlockType( 'core/test-block-c', {
apiVersion: 3,
save: ( props ) => props.attributes.text,
category: 'text',
title: 'Test Block C',
icon: 'test',
keywords: [ 'testing' ],
parent: [ 'core/test-block-b' ],
} );
registerBlockType( 'core/freeform', {
apiVersion: 3,
save: ( props ) => <RawHTML>{ props.attributes.content }</RawHTML>,
category: 'text',
title: 'Test Freeform Content Handler',
icon: 'test',
supports: {
className: false,
},
attributes: {
content: {
type: 'string',
},
},
} );
registerBlockType( 'core/test-default', {
apiVersion: 3,
category: 'text',
title: 'default',
attributes: {
modified: {
type: 'boolean',
default: false,
},
},
save: () => null,
} );
setFreeformContentHandlerName( 'core/freeform' );
setDefaultBlockName( 'core/test-default' );
cachedSelectors.forEach( ( { clear } ) => clear() );
} );
afterEach( () => {
unregisterBlockType( 'core/block' );
unregisterBlockType( 'core/test-block-a' );
unregisterBlockType( 'core/test-block-b' );
unregisterBlockType( 'core/test-block-c' );
unregisterBlockType( 'core/freeform' );
unregisterBlockType( 'core/test-default' );
setFreeformContentHandlerName( undefined );
setDefaultBlockName( undefined );
} );
describe( 'hasEditorUndo', () => {
it( 'should return true when the past history is not empty', () => {
const state = {
editor: {
past: [ {} ],
},
};
expect( hasEditorUndo( state ) ).toBe( true );
} );
it( 'should return false when the past history is empty', () => {
const state = {
editor: {
past: [],
},
};
expect( hasEditorUndo( state ) ).toBe( false );
} );
} );
describe( 'hasEditorRedo', () => {
it( 'should return true when the future history is not empty', () => {
const state = {
editor: {
future: [ {} ],
},
};
expect( hasEditorRedo( state ) ).toBe( true );
} );
it( 'should return false when the future history is empty', () => {
const state = {
editor: {
future: [],
},
};
expect( hasEditorRedo( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostNew', () => {
it( 'should return true when the post is new', () => {
const state = {
currentPost: {
status: 'auto-draft',
},
editor: {
present: {
edits: {
status: 'draft',
},
},
},
initialEdits: {},
};
expect( isEditedPostNew( state ) ).toBe( true );
} );
it( 'should return false when the post is not new', () => {
const state = {
currentPost: {
status: 'draft',
},
editor: {
present: {
edits: {
status: 'draft',
},
},
},
initialEdits: {},
};
expect( isEditedPostNew( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostDirty', () => {
it( 'should return false when blocks state not dirty nor edits exist', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
};
expect( isEditedPostDirty( state ) ).toBe( false );
} );
it( 'should return true when blocks state dirty', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: true,
value: [],
},
edits: {},
},
},
};
expect( isEditedPostDirty( state ) ).toBe( true );
} );
it( 'should return true when edits exist', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {
excerpt: 'hello world',
},
},
},
};
expect( isEditedPostDirty( state ) ).toBe( true );
} );
} );
describe( 'hasNonPostEntityChanges', () => {
it( 'should return true if there are changes to an arbitrary entity', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetDirtyEntityRecords() {
return [
{ kind: 'someKind', name: 'someName', key: 'someKey' },
];
},
};
expect( hasNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return false if there are only changes for the current post', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetDirtyEntityRecords() {
return [ { kind: 'postType', name: 'post', key: 1 } ];
},
};
expect( hasNonPostEntityChanges( state ) ).toBe( false );
} );
it( 'should return true if there are changes to multiple posts', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetDirtyEntityRecords() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'post', key: 2 },
];
},
};
expect( hasNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return true if there are changes to multiple posts of different post types', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetDirtyEntityRecords() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'wp_template', key: 1 },
];
},
};
expect( hasNonPostEntityChanges( state ) ).toBe( true );
} );
} );
describe( 'isCleanNewPost', () => {
it( 'should return true when the post is not dirty and has not been saved before', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
id: 1,
status: 'auto-draft',
},
saving: {
requesting: false,
},
};
expect( isCleanNewPost( state ) ).toBe( true );
} );
it( 'should return false when the post is not dirty but the post has been saved', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
id: 1,
status: 'draft',
},
saving: {
requesting: false,
},
};
expect( isCleanNewPost( state ) ).toBe( false );
} );
it( 'should return false when the post is dirty but the post has not been saved', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: true,
value: [],
},
edits: {},
},
},
currentPost: {
id: 1,
status: 'auto-draft',
},
saving: {
requesting: false,
},
};
expect( isCleanNewPost( state ) ).toBe( false );
} );
} );
describe( 'getCurrentPost', () => {
it( 'should return the current post', () => {
const state = {
currentPost: { id: 1 },
};
expect( getCurrentPost( state ) ).toEqual( { id: 1 } );
} );
} );
describe( 'getCurrentPostId', () => {
it( 'should return null if the post has not yet been saved', () => {
const state = {
postId: null,
};
expect( getCurrentPostId( state ) ).toBeNull();
} );
it( 'should return the current post ID', () => {
const state = {
postId: 1,
};
expect( getCurrentPostId( state ) ).toBe( 1 );
} );
} );
describe( 'getCurrentPostAttribute', () => {
it( 'should return undefined for an attribute which does not exist', () => {
const state = {
currentPost: {},
};
expect( getCurrentPostAttribute( state, 'foo' ) ).toBeUndefined();
} );
it( 'should return undefined for object prototype member', () => {
const state = {
currentPost: {},
};
expect(
getCurrentPostAttribute( state, 'valueOf' )
).toBeUndefined();
} );
it( 'should return the value of an attribute', () => {
const state = {
currentPost: {
title: 'Hello World',
},
};
expect( getCurrentPostAttribute( state, 'title' ) ).toBe(
'Hello World'
);
} );
} );
describe( 'getEditedPostAttribute', () => {
it( 'should return the current post’s slug if no edits have been made', () => {
const state = {
currentPost: { slug: 'post slug' },
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'slug' ) ).toBe(
'post slug'
);
} );
it( 'should return the latest slug if edits have been made to the post', () => {
const state = {
currentPost: { slug: 'old slug' },
editor: {
present: {
edits: {
slug: 'new slug',
},
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'slug' ) ).toBe(
'new slug'
);
} );
it( 'should return the post saved title if the title is not edited', () => {
const state = {
currentPost: {
title: 'sassel',
},
editor: {
present: {
edits: { status: 'private' },
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'title' ) ).toBe( 'sassel' );
} );
it( 'should return the edited title', () => {
const state = {
currentPost: {
title: 'sassel',
},
editor: {
present: {
edits: { title: 'youcha' },
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'title' ) ).toBe( 'youcha' );
} );
it( 'should return undefined for object prototype member', () => {
const state = {
currentPost: {},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect(
getEditedPostAttribute( state, 'valueOf' )
).toBeUndefined();
} );
it( 'should merge mergeable properties with current post value', () => {
const state = {
currentPost: {
meta: {
a: 1,
b: 1,
},
},
editor: {
present: {
edits: {
meta: {
b: 2,
},
},
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'meta' ) ).toEqual( {
a: 1,
b: 2,
} );
} );
it( 'should return the same value for mergeable properties when called multiple times', () => {
const state = {
currentPost: {
meta: {
a: 1,
b: 1,
},
},
editor: {
present: {
edits: {
meta: {
b: 2,
},
},
},
},
initialEdits: {},
};
expect( getEditedPostAttribute( state, 'meta' ) ).toBe(
getEditedPostAttribute( state, 'meta' )
);
} );
} );
describe( 'getCurrentPostLastRevisionId', () => {
it( 'should return null if the post has not yet been saved', () => {
const state = {
currentPost: {},
};
expect( getCurrentPostLastRevisionId( state ) ).toBeNull();
} );
it( 'should return the last revision ID', () => {
const state = {
currentPost: {
_links: {
'predecessor-version': [
{
id: 123,
},
],
},
},
};
expect( getCurrentPostLastRevisionId( state ) ).toBe( 123 );
} );
} );
describe( 'getCurrentPostRevisionsCount', () => {
it( 'should return 0 if the post has no revisions', () => {
const state = {
currentPost: {},
};
expect( getCurrentPostRevisionsCount( state ) ).toBe( 0 );
} );
it( 'should return the number of revisions', () => {
const state = {
currentPost: {
_links: {
'version-history': [
{
count: 5,
},
],
},
},
};
expect( getCurrentPostRevisionsCount( state ) ).toBe( 5 );
} );
} );
describe( 'getCurrentPostType', () => {
it( 'should return the post type', () => {
const state = {
postType: 'post',
};
expect( getCurrentPostType( state ) ).toBe( 'post' );
} );
} );
describe( 'getPostEdits', () => {
it( 'should return the post edits', () => {
const state = {
editor: {
present: {
edits: { title: 'terga' },
},
},
initialEdits: {},
};
expect( getPostEdits( state ) ).toEqual( { title: 'terga' } );
} );
it( 'should return value from initial edits', () => {
const state = {
editor: {
present: {
edits: {},
},
},
initialEdits: { title: 'terga' },
};
expect( getPostEdits( state ) ).toEqual( { title: 'terga' } );
} );
it( 'should prefer value from edits over initial edits', () => {
const state = {
editor: {
present: {
edits: { title: 'werga' },
},
},
initialEdits: { title: 'terga' },
};
expect( getPostEdits( state ) ).toEqual( { title: 'werga' } );
} );
} );
describe( 'getEditedPostVisibility', () => {
it( 'should return public by default', () => {
const state = {
currentPost: {
status: 'draft',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( getEditedPostVisibility( state ) ).toBe( 'public' );
} );
it( 'should return private for private posts', () => {
const state = {
currentPost: {
status: 'private',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( getEditedPostVisibility( state ) ).toBe( 'private' );
} );
it( 'should return private for password for password protected posts', () => {
const state = {
currentPost: {
status: 'draft',
password: 'chicken',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( getEditedPostVisibility( state ) ).toBe( 'password' );
} );
it( 'should use the edited status and password if edits present', () => {
const state = {
currentPost: {
status: 'draft',
password: 'chicken',
},
editor: {
present: {
edits: {
status: 'private',
password: null,
},
},
},
initialEdits: {},
};
expect( getEditedPostVisibility( state ) ).toBe( 'private' );
} );
} );
describe( 'isCurrentPostPending', () => {
it( 'should return true for posts in pending state', () => {
const state = {
currentPost: {
status: 'pending',
},
};
expect( isCurrentPostPending( state ) ).toBe( true );
} );
it( 'should return false for draft posts', () => {
const state = {
currentPost: {
status: 'draft',
},
};
expect( isCurrentPostPending( state ) ).toBe( false );
} );
it( 'should return false if status is unknown', () => {
const state = {
currentPost: {},
};
expect( isCurrentPostPending( state ) ).toBe( false );
} );
} );
describe( 'isCurrentPostPublished', () => {
it( 'should return true for public posts', () => {
const state = {
currentPost: {
status: 'publish',
},
};
expect( isCurrentPostPublished( state ) ).toBe( true );
} );
it( 'should return true for private posts', () => {
const state = {
currentPost: {
status: 'private',
},
};
expect( isCurrentPostPublished( state ) ).toBe( true );
} );
it( 'should return false for draft posts', () => {
const state = {
currentPost: {
status: 'draft',
},
};
expect( isCurrentPostPublished( state ) ).toBe( false );
} );
it( 'should return true for old scheduled posts', () => {
const state = {
currentPost: {
status: 'future',
date: '2016-05-30T17:21:39',
},
};
expect( isCurrentPostPublished( state ) ).toBe( true );
} );
} );
describe( 'isCurrentPostScheduled', () => {
it( 'should return true for future scheduled posts', () => {
const state = {
currentPost: {
status: 'future',
date: '2100-05-30T17:21:39',
},
};
expect( isCurrentPostScheduled( state ) ).toBe( true );
} );
it( 'should return false for old scheduled posts that were already published', () => {
const state = {
currentPost: {
status: 'future',
date: '2016-05-30T17:21:39',
},
};
expect( isCurrentPostScheduled( state ) ).toBe( false );
} );
it( 'should return false for auto draft posts', () => {
const state = {
currentPost: {
status: 'auto-draft',
},
};
expect( isCurrentPostScheduled( state ) ).toBe( false );
} );
it( 'should return false if status is unknown', () => {
const state = {
currentPost: {},
};
expect( isCurrentPostScheduled( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostPublishable', () => {
it( 'should return true for pending posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'pending',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( true );
} );
it( 'should return true for draft posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'draft',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( true );
} );
it( 'should return false for published posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'publish',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( false );
} );
it( 'should return true for published, dirty posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: true,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'publish',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( true );
} );
it( 'should return false for private posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'private',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( false );
} );
it( 'should return false for scheduled posts', () => {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
currentPost: {
status: 'future',
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( false );
} );
it( 'should return true for dirty posts with usable title', () => {
const state = {
currentPost: {
status: 'private',
},
editor: {
present: {
blocks: {
isDirty: true,
value: [],
},
edits: {},
},
},
saving: {
requesting: false,
},
};
expect( isEditedPostPublishable( state ) ).toBe( true );
} );
} );
describe( 'isPostSavingLocked', () => {
it( 'should return true if the post has postSavingLocks', () => {
const state = {
postSavingLock: { example: true },
currentPost: {},
saving: {},
};
expect( isPostSavingLocked( state ) ).toBe( true );
} );
it( 'should return false if the post has no postSavingLocks', () => {
const state = {
postSavingLock: {},
currentPost: {},
saving: {},
};
expect( isPostSavingLocked( state ) ).toBe( false );
} );
} );
describe( 'isPostAutosavingLocked', () => {
it( 'should return true if the post has postAutosavingLocks', () => {
const state = {
postAutosavingLock: { example: true },
currentPost: {},
saving: {},
};
expect( isPostAutosavingLocked( state ) ).toBe( true );
} );
it( 'should return false if the post has no postAutosavingLocks', () => {
const state = {
postAutosavingLock: {},
currentPost: {},
saving: {},
};
expect( isPostAutosavingLocked( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostSaveable', () => {
it( 'should return false if the post has no title, excerpt, content', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( false );
} );
it( 'should return false if the post has a title but save already in progress', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
saving: {
pending: true,
},
};
expect( isEditedPostSaveable( state ) ).toBe( false );
} );
it( 'should return true if the post has a title', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( true );
} );
it( 'should return true if the post has an excerpt', () => {
const state = {
editor: {
present: {
blocks: {
byClientId: {},
attributes: {},
order: {},
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
excerpt: 'sassel',
},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( true );
} );
it( 'should return true if the post has content', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/test-block-a',
isValid: true,
attributes: {
text: '',
},
},
],
},
edits: {
content: () => {},
},
},
},
initialEdits: {},
currentPost: {},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( true );
} );
it( 'should return false if the post has no title, excerpt and empty classic block', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
],
},
edits: {},
},
},
initialEdits: {},
currentPost: {},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( false );
} );
it( 'should return true if the post has a title and empty classic block', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
saving: {},
};
expect( isEditedPostSaveable( state ) ).toBe( true );
} );
} );
describe( 'isEditedPostAutosaveable', () => {
it( 'should return false if existing autosaves have not yet been fetched', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
postAutosavingLock: {},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return false;
},
getAutosave() {
return {
title: 'sassel',
};
},
};
expect( isEditedPostAutosaveable( state ) ).toBe( false );
} );
it( 'should return false if the post is not saveable', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
postAutosavingLock: {},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {
return {
title: 'sassel',
};
},
};
expect( isEditedPostAutosaveable( state ) ).toBe( false );
} );
it( 'should return true if there is no autosave', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {},
postAutosavingLock: {},
};
expect( isEditedPostAutosaveable( state ) ).toBe( true );
} );
it( 'should return false if none of title, excerpt, or content have changed', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
isDirty: false,
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'foo',
excerpt: 'foo',
},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {
return {
title: 'foo',
excerpt: 'foo',
};
},
postAutosavingLock: {},
};
expect( isEditedPostAutosaveable( state ) ).toBe( false );
} );
it( 'should return true if content has changes', () => {
const state = {
editor: {
present: {
edits: {
content: () => 'new-content',
},
},
},
currentPost: {
title: 'foo',
excerpt: 'foo',
},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {
return {
title: 'foo',
excerpt: 'foo',
};
},
postAutosavingLock: {},
};
expect( isEditedPostAutosaveable( state ) ).toBe( true );
} );
it( 'should return true if title or excerpt have changed', () => {
const fields = [ 'title', 'excerpt' ];
for ( const variantField of fields ) {
for ( const constantField of fields.filter(
( f ) => f !== variantField
) ) {
const state = {
editor: {
present: {
blocks: {
isDirty: false,
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'foo',
content: 'foo',
},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {
return {
[ constantField ]: 'foo',
[ variantField ]: 'bar',
};
},
postAutosavingLock: {},
};
expect( isEditedPostAutosaveable( state ) ).toBe( true );
}
}
} );
it( 'should return false if autosaving is locked', () => {
const state = {
currentPost: {},
saving: {},
postAutosavingLock: { example: true },
};
expect( isEditedPostAutosaveable( state ) ).toBe( false );
} );
it( 'should return false if post type does not support autosave', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
title: 'sassel',
},
saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
},
getAutosave() {},
postAutosavingLock: {},
postType: 'without-autosave',
};
expect( isEditedPostAutosaveable( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostEmpty', () => {
it( 'should return true if no blocks and no content', () => {
const state = {
editor: {
present: {
edits: {},
},
},
initialEdits: {},
currentPost: {
content: '',
},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return false if blocks', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/test-block-a',
isValid: true,
attributes: {
text: '',
},
},
],
},
edits: {
content: () => {},
},
},
},
initialEdits: {},
currentPost: {},
};
expect( isEditedPostEmpty( state ) ).toBe( false );
} );
it( 'should account for filtering logic of getBlocksForSerialization', () => {
// Note: As an optimization, isEditedPostEmpty avoids using the
// getBlocksForSerialization selector and instead makes assumptions
// about its filtering. The behavior should still be reflected.
//
// See: https://github.com/WordPress/gutenberg/pull/13086
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 'block1',
name: 'core/test-default',
attributes: {
modified: false,
},
},
],
},
edits: {
content: () => {},
},
},
},
initialEdits: {},
currentPost: {},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return true if blocks, but empty content edit', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/test-block-a',
isValid: true,
attributes: {
text: '',
},
},
],
},
edits: {
content: '',
},
},
},
initialEdits: {},
currentPost: {
content: '',
},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return true if the post has an empty content property', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
content: '',
},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return false if edits include a non-empty content property', () => {
const state = {
editor: {
present: {
blocks: {
value: [],
},
edits: {
content: 'sassel',
},
},
},
initialEdits: {},
currentPost: {},
};
expect( isEditedPostEmpty( state ) ).toBe( false );
} );
it( 'should return true if empty classic block', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
],
},
edits: {
content: () => {},
},
},
},
initialEdits: {},
currentPost: {},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return true if empty content freeform block', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
content: '',
},
};
expect( isEditedPostEmpty( state ) ).toBe( true );
} );
it( 'should return false if non-empty content freeform block', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: 'Test Data',
},
},
],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
content: 'Test Data',
},
};
expect( isEditedPostEmpty( state ) ).toBe( false );
} );
it( 'should return false for multiple empty freeform blocks', () => {
const state = {
editor: {
present: {
blocks: {
value: [
{
clientId: 123,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
{
clientId: 456,
name: 'core/freeform',
isValid: true,
attributes: {
content: '',
},
},
],
},
edits: {},
},
},
initialEdits: {},
currentPost: {
content: '\n\n',
},
};
expect( isEditedPostEmpty( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostBeingScheduled', () => {
it( 'should return true for posts with a future date', () => {
const time = Date.now() + 1000 * 3600 * 24 * 7; // 7 days in the future.
const date = new Date( time );
const state = {
editor: {
present: {
edits: { date: date.toUTCString() },
},
},
initialEdits: {},
};
expect( isEditedPostBeingScheduled( state ) ).toBe( true );
} );
it( 'should return false for posts with an old date', () => {
const state = {
editor: {
present: {
edits: { date: '2016-05-30T17:21:39' },
},
},
initialEdits: {},
};
expect( isEditedPostBeingScheduled( state ) ).toBe( false );
} );
} );
describe( 'isEditedPostDateFloating', () => {
it( 'should return true for draft posts where the date matches the modified date', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '2018-09-27T01:23:45.678Z',
status: 'draft',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( true );
} );
it( 'should return true for auto-draft posts where the date matches the modified date', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '2018-09-27T01:23:45.678Z',
status: 'auto-draft',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( true );
} );
it( 'should return false for draft posts where the date does not match the modified date', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '1970-01-01T00:00:00.000Z',
status: 'draft',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( false );
} );
it( 'should return false for auto-draft posts where the date does not match the modified date', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '1970-01-01T00:00:00.000Z',
status: 'auto-draft',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( false );
} );
it( 'should return false for published posts', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '2018-09-27T01:23:45.678Z',
status: 'publish',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( false );
} );
it( 'should return true for pending posts', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '2018-09-27T01:23:45.678Z',
status: 'pending',
},
editor: {
present: {
edits: {},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( true );
} );
it( 'should return false for private posts even if the edited status is "draft"', () => {
const state = {
currentPost: {
date: '2018-09-27T01:23:45.678Z',
modified: '2018-09-27T01:23:45.678Z',
status: 'private',
},
editor: {
present: {
edits: {
status: 'draft',
},
},
},
initialEdits: {},
};
expect( isEditedPostDateFloating( state ) ).toBe( false );
} );
} );
describe( 'isSavingPost', () => {
it( 'should return true if the post is currently being saved', () => {
const state = {
saving: {
pending: true,
},
};
expect( isSavingPost( state ) ).toBe( true );
} );
it( 'should return false if the post is not currently being saved', () => {
const state = {
saving: {
pending: false,
},
};
expect( isSavingPost( state ) ).toBe( false );
} );
} );
describe( 'isSavingNonPostEntityChanges', () => {
it( 'should return true if changes to an arbitrary entity are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'someKind', name: 'someName', key: 'someKey' },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return false if the only changes being saved are for the current post', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [ { kind: 'postType', name: 'post', key: 1 } ];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( false );
} );
it( 'should return true if changes to multiple posts are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'post', key: 2 },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return true if changes to multiple posts of different post types are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'wp_template', key: 1 },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
} );
describe( 'didPostSaveRequestSucceed', () => {
it( 'should return true if the post save request is successful', () => {
const state = {
saving: {
successful: true,
},
};
expect( didPostSaveRequestSucceed( state ) ).toBe( true );
} );
it( 'should return false if the post save request has failed', () => {
const state = {
saving: {
successful: false,
},
};
expect( didPostSaveRequestSucceed( state ) ).toBe( false );
} );
} );
describe( 'didPostSaveRequestFail', () => {
it( 'should return true if the post save request has failed', () => {
const state = {
saving: {
error: 'error',
},
};
expect( didPostSaveRequestFail( state ) ).toBe( true );
} );
it( 'should return false if the post save request is successful', () => {
const state = {
saving: {
error: false,
},
};
expect( didPostSaveRequestFail( state ) ).toBe( false );
} );
} );
describe( 'getSuggestedPostFormat', () => {
it( 'returns null if cannot be determined', () => {
const state = {
getBlocks() {
return [];
},
};
expect( getSuggestedPostFormat( state ) ).toBeNull();
} );
it( 'return null if only one block of type `core/embed` and provider not matched', () => {
const state = {
getBlocks() {
return [
{
clientId: 567,
name: 'core/embed',
attributes: {
providerNameSlug: 'instagram',
},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBeNull();
} );
it( 'return null if only one block of type `core/embed` and provider not exists', () => {
const state = {
getBlocks() {
return [
{
clientId: 567,
name: 'core/embed',
attributes: {},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBeNull();
} );
it( 'returns null if there is more than one block in the post', () => {
const state = {
getBlocks() {
return [
{
clientId: 123,
name: 'core/image',
attributes: {},
innerBlocks: [],
},
{
clientId: 456,
name: 'core/quote',
attributes: {},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBeNull();
} );
it( 'returns Image if the first block is of type `core/image`', () => {
const state = {
getBlocks() {
return [
{
clientId: 123,
name: 'core/image',
attributes: {},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBe( 'image' );
} );
it( 'returns Quote if the first block is of type `core/quote`', () => {
const state = {
getBlocks() {
return [
{
clientId: 456,
name: 'core/quote',
attributes: {},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBe( 'quote' );
} );
it( 'returns Video if the first block is of type `core/embed from youtube`', () => {
const state = {
getBlocks() {
return [
{
clientId: 567,
name: 'core/embed',
attributes: {
providerNameSlug: 'youtube',
},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBe( 'video' );
} );
it( 'returns Audio if the first block is of type `core/embed from soundcloud`', () => {
const state = {
getBlocks() {
return [
{
clientId: 567,
name: 'core/embed',
attributes: {
providerNameSlug: 'soundcloud',
},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBe( 'audio' );
} );
it( 'returns Quote if the first block is of type `core/quote` and second is of type `core/paragraph`', () => {
const state = {
getBlocks() {
return [
{
clientId: 456,
name: 'core/quote',
attributes: {},
innerBlocks: [],
},
{
clientId: 789,
name: 'core/paragraph',
attributes: {},
innerBlocks: [],
},
];
},
};
expect( getSuggestedPostFormat( state ) ).toBe( 'quote' );
} );
} );
describe( 'getEditedPostContent', () => {
let originalDefaultBlockName;
beforeAll( () => {
originalDefaultBlockName = getDefaultBlockName();
registerBlockType( 'core/default', {
apiVersion: 3,
category: 'text',
title: 'default',
attributes: {
modified: {
type: 'boolean',
default: false,
},
},
save: () => null,
} );
setDefaultBlockName( 'core/default' );
} );
afterAll( () => {
setDefaultBlockName( originalDefaultBlockName );
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );
it( 'serializes blocks, if any', () => {
const block = createBlock( 'core/block' );
const state = {
editor: {
present: {
blocks: {
value: [ block ],
},
edits: {
content: 'custom edit',
},
},
},
initialEdits: {},
currentPost: {},
};
const content = getEditedPostContent( state );
expect( content ).toBe( '<!-- wp:block /-->' );
} );
it( 'returns serialization of blocks', () => {
const block = createBlock( 'core/block' );
const state = {
editor: {
present: {
blocks: {
value: [ block ],
},
edits: {},
},
},
initialEdits: {},
currentPost: {},
};
const content = getEditedPostContent( state );
expect( content ).toBe( '<!-- wp:block /-->' );
} );
it( "returns removep'd serialization of blocks for single unknown", () => {
const unknownBlock = createBlock( 'core/freeform', {
content: '<p>foo</p>',
} );
const state = {
editor: {
present: {
blocks: {
value: [ unknownBlock ],
},
edits: {},
},
},
initialEdits: {},
currentPost: {},
};
const content = getEditedPostContent( state );
expect( content ).toBe( 'foo' );
} );
it( "returns non-removep'd serialization of blocks for multiple unknown", () => {
const firstUnknown = createBlock( 'core/freeform', {
content: '<p>foo</p>',
} );
const secondUnknown = createBlock( 'core/freeform', {
content: '<p>bar</p>',
} );
const state = {
editor: {
present: {
blocks: {
value: [ firstUnknown, secondUnknown ],
},
edits: {},
},
},
initialEdits: {},
currentPost: {},
};
const content = getEditedPostContent( state );
expect( content ).toBe( '<p>foo</p>\n\n<p>bar</p>' );
} );
it( 'returns empty string for single unmodified default block', () => {
const defaultBlock = createBlock( getDefaultBlockName() );
const state = {
editor: {
present: {
blocks: {
value: [ defaultBlock