@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
656 lines (556 loc) • 17.1 kB
JavaScript
/**
* WordPress dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { createRegistry } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { store as preferencesStore } from '@wordpress/preferences';
/**
* Internal dependencies
*/
import * as actions from '../actions';
import { store as editorStore } from '..';
const postId = 44;
const postTypeConfig = {
kind: 'postType',
name: 'post',
baseURL: '/wp/v2/posts',
transientEdits: { blocks: true, selection: true },
mergedEdits: { meta: true },
rawAttributes: [ 'title', 'excerpt', 'content' ],
};
const postTypeEntity = {
slug: 'post',
rest_base: 'posts',
labels: {
item_updated: 'Updated Post',
item_published: 'Post published',
item_reverted_to_draft: 'Post reverted to draft.',
item_trashed: 'Post trashed.',
},
};
function createRegistryWithStores() {
// Create a registry.
const registry = createRegistry();
// Register stores.
registry.register( blockEditorStore );
registry.register( coreStore );
registry.register( editorStore );
registry.register( noticesStore );
registry.register( preferencesStore );
// Register post type entity.
registry.dispatch( coreStore ).addEntities( [ postTypeConfig ] );
// Store post type entity.
registry
.dispatch( coreStore )
.receiveEntityRecords( 'root', 'postType', [ postTypeEntity ] );
return registry;
}
const getMethod = ( options ) =>
options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET';
describe( 'Post actions', () => {
describe( 'savePost()', () => {
it( 'saves a modified post', async () => {
const post = {
id: postId,
type: 'post',
title: 'bar',
content: 'bar',
excerpt: 'crackers',
status: 'draft',
};
// Mock apiFetch response.
apiFetch.setFetchHandler( async ( options ) => {
const method = getMethod( options );
const { path, data } = options;
if (
method === 'PUT' &&
path.startsWith( `/wp/v2/posts/${ postId }` )
) {
return { ...post, ...data };
} else if (
// This URL is requested by the actions dispatched in this test.
// They are safe to ignore and are only listed here to avoid triggeringan error.
method === 'GET' &&
path.startsWith( '/wp/v2/types/post' )
) {
return {
json: () => Promise.resolve( {} ),
};
}
throw {
code: 'unknown_path',
message: `Unknown path: ${ method } ${ path }`,
};
} );
// Create registry.
const registry = createRegistryWithStores();
// Store post.
registry
.dispatch( coreStore )
.receiveEntityRecords( 'postType', 'post', post );
// Setup editor with post and initial edits.
registry.dispatch( editorStore ).setupEditor( post, {
content: 'new bar',
} );
// Check that the post is dirty.
expect( registry.select( editorStore ).isEditedPostDirty() ).toBe(
true
);
// Save the post.
await registry.dispatch( editorStore ).savePost();
// Check the new content.
const content = registry
.select( editorStore )
.getEditedPostContent();
expect( content ).toBe( 'new bar' );
// Check that the post is no longer dirty.
expect( registry.select( editorStore ).isEditedPostDirty() ).toBe(
false
);
// Check that a success notice has been shown.
const notices = registry.select( noticesStore ).getNotices();
expect( notices ).toMatchObject( [
{
status: 'success',
content: 'Draft saved.',
},
] );
} );
} );
describe( 'autosave()', () => {
it( 'autosaves a modified post', async () => {
const post = {
id: postId,
type: 'post',
title: 'bar',
content: 'bar',
excerpt: 'crackers',
status: 'draft',
};
// Mock apiFetch response.
apiFetch.setFetchHandler( async ( options ) => {
const method = getMethod( options );
const { path, data } = options;
if (
method === 'GET' &&
path.startsWith( '/wp/v2/users/me' )
) {
return { id: 1 };
} else if (
path.startsWith( `/wp/v2/posts/${ postId }/autosaves` )
) {
if ( method === 'POST' ) {
return { ...post, ...data };
} else if ( method === 'GET' ) {
return [];
}
} else if ( method === 'GET' ) {
// These URLs are requested by the actions dispatched in this test.
// They are safe to ignore and are only listed here to avoid triggeringan error.
if (
path.startsWith( '/wp/v2/types/post' ) ||
path.startsWith( `/wp/v2/posts/${ postId }` )
) {
return {
json: () => Promise.resolve( {} ),
};
}
}
throw {
code: 'unknown_path',
message: `Unknown path: ${ method } ${ path }`,
};
} );
// Create registry.
const registry = createRegistryWithStores();
// Set current user.
registry.dispatch( coreStore ).receiveCurrentUser( { id: 1 } );
// Store post.
registry
.dispatch( coreStore )
.receiveEntityRecords( 'postType', 'post', post );
// Setup editor with post and initial edits.
registry.dispatch( editorStore ).setupEditor( post, {
content: 'new bar',
} );
// Check that the post is dirty.
expect( registry.select( editorStore ).isEditedPostDirty() ).toBe(
true
);
// Autosave the post.
await registry.dispatch( editorStore ).autosave();
// Check the new content.
const content = registry
.select( editorStore )
.getEditedPostContent();
expect( content ).toBe( 'new bar' );
// Check that the post is no longer dirty.
expect( registry.select( editorStore ).isEditedPostDirty() ).toBe(
false
);
// Check that no notice has been shown on autosave.
const notices = registry.select( noticesStore ).getNotices();
expect( notices ).toMatchObject( [] );
} );
} );
describe( 'trashPost()', () => {
it( 'trashes a post', async () => {
const post = {
id: postId,
type: 'post',
content: 'foo',
status: 'publish',
};
let gotTrashed = false;
// Mock apiFetch response.
apiFetch.setFetchHandler( async ( options ) => {
const method = getMethod( options );
const { path, data } = options;
if ( path.startsWith( `/wp/v2/posts/${ postId }` ) ) {
if ( method === 'DELETE' ) {
gotTrashed = true;
return { ...post, status: 'trash' };
} else if ( method === 'PUT' ) {
return {
...post,
...( gotTrashed && { status: 'trash' } ),
...data,
};
}
// This URL is requested by the actions dispatched in this test.
// They are safe to ignore and are only listed here to avoid triggeringan error.
} else if (
method === 'GET' &&
path.startsWith( '/wp/v2/types/post' )
) {
return {
json: () => Promise.resolve( {} ),
};
}
throw {
code: 'unknown_path',
message: `Unknown path: ${ path }`,
};
} );
// Create registry.
const registry = createRegistryWithStores();
// Store post.
registry
.dispatch( coreStore )
.receiveEntityRecords( 'postType', 'post', post );
// Setup editor with post.
registry.dispatch( editorStore ).setupEditor( post );
// Trash the post.
await registry.dispatch( editorStore ).trashPost();
// Check that there are no notices.
const notices = registry.select( noticesStore ).getNotices();
expect( notices ).toMatchObject( [
{
status: 'success',
content: 'Post trashed.',
},
] );
// Check the new status.
const { status } = registry.select( editorStore ).getCurrentPost();
expect( status ).toBe( 'trash' );
} );
it( 'sets deleting state', async () => {
const post = {
id: postId,
type: 'post',
content: 'foo',
status: 'publish',
};
const dispatch = Object.assign( jest.fn(), {
savePost: jest.fn(),
} );
const select = {
getCurrentPostType: () => 'post',
getCurrentPost: () => post,
};
const registry = {
dispatch: () => ( {
removeNotice: jest.fn(),
createErrorNotice: jest.fn(),
} ),
resolveSelect: () => ( {
getPostType: () => ( {
rest_namespace: 'wp/v2',
rest_base: 'posts',
} ),
} ),
};
apiFetch.setFetchHandler( async () => {
return { ...post, status: 'trash' };
} );
await actions.trashPost()( { select, dispatch, registry } );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'REQUEST_POST_DELETE_START',
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'REQUEST_POST_DELETE_FINISH',
} );
} );
} );
} );
describe( 'Editor actions', () => {
describe( 'setupEditor()', () => {
it( 'should setup the editor', () => {
// Create registry.
const registry = createRegistryWithStores();
registry
.dispatch( editorStore )
.setupEditor( { id: 10, type: 'post' } );
expect( registry.select( editorStore ).getCurrentPostId() ).toBe(
10
);
} );
} );
describe( 'lockPostSaving', () => {
it( 'should return the LOCK_POST_SAVING action', () => {
const result = actions.lockPostSaving( 'test' );
expect( result ).toEqual( {
type: 'LOCK_POST_SAVING',
lockName: 'test',
} );
} );
} );
describe( 'unlockPostSaving', () => {
it( 'should return the UNLOCK_POST_SAVING action', () => {
const result = actions.unlockPostSaving( 'test' );
expect( result ).toEqual( {
type: 'UNLOCK_POST_SAVING',
lockName: 'test',
} );
} );
} );
describe( 'lockPostAutosaving', () => {
it( 'should return the LOCK_POST_AUTOSAVING action', () => {
const result = actions.lockPostAutosaving( 'test' );
expect( result ).toEqual( {
type: 'LOCK_POST_AUTOSAVING',
lockName: 'test',
} );
} );
} );
describe( 'unlockPostAutosaving', () => {
it( 'should return the UNLOCK_POST_AUTOSAVING action', () => {
const result = actions.unlockPostAutosaving( 'test' );
expect( result ).toEqual( {
type: 'UNLOCK_POST_AUTOSAVING',
lockName: 'test',
} );
} );
} );
describe( 'enablePublishSidebar', () => {
it( 'enables the publish sidebar', () => {
const registry = createRegistryWithStores();
// Starts off as `undefined` as a default hasn't been set.
expect(
registry.select( editorStore ).isPublishSidebarEnabled()
).toBe( false );
registry.dispatch( editorStore ).enablePublishSidebar();
expect(
registry.select( editorStore ).isPublishSidebarEnabled()
).toBe( true );
} );
} );
describe( 'disablePublishSidebar', () => {
it( 'disables the publish sidebar', () => {
const registry = createRegistryWithStores();
// Enable it to start with so that can test it flipping from `true` to `false`.
registry.dispatch( editorStore ).enablePublishSidebar();
expect(
registry.select( editorStore ).isPublishSidebarEnabled()
).toBe( true );
registry.dispatch( editorStore ).disablePublishSidebar();
expect(
registry.select( editorStore ).isPublishSidebarEnabled()
).toBe( false );
} );
} );
describe( 'toggleEditorPanelEnabled', () => {
it( 'toggles panels to be enabled and not enabled', () => {
const registry = createRegistryWithStores();
// This will switch it off, since the default is on.
registry
.dispatch( editorStore )
.toggleEditorPanelEnabled( 'control-panel' );
expect(
registry
.select( editorStore )
.isEditorPanelEnabled( 'control-panel' )
).toBe( false );
// Switch it on again.
registry
.dispatch( editorStore )
.toggleEditorPanelEnabled( 'control-panel' );
expect(
registry
.select( editorStore )
.isEditorPanelEnabled( 'control-panel' )
).toBe( true );
} );
} );
describe( 'toggleEditorPanelOpened', () => {
it( 'toggles panels open and closed', () => {
const registry = createRegistryWithStores();
// This will open it, since the default is closed.
registry
.dispatch( editorStore )
.toggleEditorPanelOpened( 'control-panel' );
expect(
registry
.select( editorStore )
.isEditorPanelOpened( 'control-panel' )
).toBe( true );
// Close it.
registry
.dispatch( editorStore )
.toggleEditorPanelOpened( 'control-panel' );
expect(
registry
.select( editorStore )
.isEditorPanelOpened( 'control-panel' )
).toBe( false );
} );
} );
describe( 'switchEditorMode', () => {
let registry;
beforeEach( () => {
registry = createRegistryWithStores();
} );
it( 'to visual', () => {
// Switch to text first, since the default is visual.
registry.dispatch( editorStore ).switchEditorMode( 'text' );
expect( registry.select( editorStore ).getEditorMode() ).toEqual(
'text'
);
registry.dispatch( editorStore ).switchEditorMode( 'visual' );
expect( registry.select( editorStore ).getEditorMode() ).toEqual(
'visual'
);
} );
it( 'to text', () => {
// It defaults to visual.
expect( registry.select( editorStore ).getEditorMode() ).toEqual(
'visual'
);
// Add a selected client id and make sure it's there.
const clientId = 'clientId_1';
registry.dispatch( blockEditorStore ).selectionChange( clientId );
expect(
registry.select( blockEditorStore ).getSelectedBlockClientId()
).toEqual( clientId );
registry.dispatch( editorStore ).switchEditorMode( 'text' );
expect(
registry.select( blockEditorStore ).getSelectedBlockClientId()
).toBeNull();
expect( registry.select( editorStore ).getEditorMode() ).toEqual(
'text'
);
} );
it( 'should turn off distraction free mode when switching to code editor', () => {
registry
.dispatch( preferencesStore )
.set( 'core', 'distractionFree', true );
registry.dispatch( editorStore ).switchEditorMode( 'text' );
expect(
registry
.select( preferencesStore )
.get( 'core', 'distractionFree' )
).toBe( false );
} );
} );
describe( 'toggleDistractionFree', () => {
it( 'should properly update settings to prevent layout corruption when enabling distraction free mode', () => {
const registry = createRegistryWithStores();
// Enable everything that shouldn't be enabled in distraction free mode.
registry
.dispatch( preferencesStore )
.set( 'core', 'fixedToolbar', true );
registry.dispatch( editorStore ).setIsListViewOpened( true );
// Initial state is falsy.
registry.dispatch( editorStore ).toggleDistractionFree();
expect(
registry
.select( preferencesStore )
.get( 'core', 'fixedToolbar' )
).toBe( true );
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
false
);
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
false
);
expect(
registry
.select( preferencesStore )
.get( 'core', 'distractionFree' )
).toBe( true );
} );
} );
describe( 'setIsInserterOpened', () => {
it( 'should open and close the inserter', () => {
const registry = createRegistryWithStores();
registry.dispatch( editorStore ).setIsInserterOpened( true );
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
true
);
registry.dispatch( editorStore ).setIsInserterOpened( false );
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
false
);
} );
it( 'the list view should close when the inserter is opened', () => {
const registry = createRegistryWithStores();
registry.dispatch( editorStore ).setIsListViewOpened( true );
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
true
);
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
false
);
registry.dispatch( editorStore ).setIsInserterOpened( true );
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
true
);
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
false
);
} );
} );
describe( 'setIsListViewOpened', () => {
it( 'should open and close the list view', () => {
const registry = createRegistryWithStores();
registry.dispatch( editorStore ).setIsListViewOpened( true );
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
true
);
registry.dispatch( editorStore ).setIsListViewOpened( false );
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
false
);
} );
it( 'the inserter should close when the list view is opened', () => {
const registry = createRegistryWithStores();
registry.dispatch( editorStore ).setIsInserterOpened( true );
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
true
);
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
false
);
registry.dispatch( editorStore ).setIsListViewOpened( true );
expect( registry.select( editorStore ).isListViewOpened() ).toBe(
true
);
expect( registry.select( editorStore ).isInserterOpened() ).toBe(
false
);
} );
} );
} );