UNPKG

@wordpress/editor

Version:
2,459 lines (2,189 loc) 58.2 kB
/** * 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