Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add test coverage for create
  • Loading branch information
getdave committed Dec 16, 2025
commit ace2a7eb066b61f6a8931a3642d872faa41883cf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useCallback,
createInterpolateElement,
} from '@wordpress/element';
import { useEntityRecords, store as coreStore } from '@wordpress/core-data';
import { useEntityRecords } from '@wordpress/core-data';
import { useDispatch } from '@wordpress/data';
import { SelectControl, Button } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
Expand All @@ -18,7 +18,7 @@ import { store as noticesStore } from '@wordpress/notices';
* Internal dependencies
*/
import { createTemplatePartId } from '../../template-part/edit/utils/create-template-part-id';
import { getUniqueTemplatePartTitle, getCleanTemplatePartSlug } from './utils';
import useCreateOverlayTemplatePart from './use-create-overlay';

/**
* Overlay Template Part Selector component.
Expand All @@ -42,7 +42,6 @@ export default function OverlayTemplatePartSelector( {
per_page: -1,
} );

const { saveEntityRecord } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );

// Track if we're currently creating a new overlay
Expand All @@ -58,6 +57,10 @@ export default function OverlayTemplatePartSelector( {
);
}, [ templateParts ] );

// Hook to create overlay template part
const createOverlayTemplatePart =
useCreateOverlayTemplatePart( overlayTemplateParts );

// Build options for SelectControl
const options = useMemo( () => {
const baseOptions = [
Expand Down Expand Up @@ -122,34 +125,6 @@ export default function OverlayTemplatePartSelector( {
} );
};

// Create a new overlay template part
const createOverlayTemplatePart = useCallback( async () => {
// Generate unique name using only overlay area template parts
// Filter to only include template parts with titles for uniqueness check
const templatePartsWithTitles = overlayTemplateParts.filter(
( templatePart ) => templatePart.title?.rendered
);
const uniqueTitle = getUniqueTemplatePartTitle(
__( 'Overlay' ),
templatePartsWithTitles
);
const cleanSlug = getCleanTemplatePartSlug( uniqueTitle );

// Create the template part
const templatePart = await saveEntityRecord(
'postType',
'wp_template_part',
{
slug: cleanSlug,
title: uniqueTitle,
area: 'overlay',
},
{ throwOnError: true }
);

return templatePart;
}, [ overlayTemplateParts, saveEntityRecord ] );

const handleCreateOverlay = useCallback( async () => {
try {
setIsCreating( true );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,41 @@ import userEvent from '@testing-library/user-event';
* WordPress dependencies
*/
import { useEntityRecords } from '@wordpress/core-data';
import { useDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import OverlayTemplatePartSelector from '../overlay-template-part-selector';
import useCreateOverlayTemplatePart from '../use-create-overlay';

// Mock useEntityRecords
jest.mock( '@wordpress/core-data', () => {
const actual = jest.requireActual( '@wordpress/core-data' );
return {
...actual,
useEntityRecords: jest.fn(),
};
} );
jest.mock( '@wordpress/core-data', () => ( {
useEntityRecords: jest.fn(),
store: {},
} ) );

// Mock useCreateOverlayTemplatePart hook
jest.mock( '../use-create-overlay', () => ( {
__esModule: true,
default: jest.fn(),
} ) );

// Mock useDispatch specifically to avoid needing to set up full data store
jest.mock( '@wordpress/data', () => ( {
useDispatch: jest.fn(),
createSelector: jest.fn( ( fn ) => fn ),
createRegistrySelector: jest.fn( ( fn ) => fn ),
createReduxStore: jest.fn( () => ( {} ) ),
combineReducers: jest.fn( ( reducers ) => ( state = {}, action ) => {
const newState = {};
Object.keys( reducers ).forEach( ( key ) => {
newState[ key ] = reducers[ key ]( state[ key ], action );
} );
return newState;
} ),
register: jest.fn(),
} ) );

const mockSetAttributes = jest.fn();
const mockOnNavigateToEntityRecord = jest.fn();
Expand Down Expand Up @@ -69,13 +90,23 @@ const allTemplateParts = [
];

describe( 'OverlayTemplatePartSelector', () => {
const mockCreateOverlayTemplatePart = jest.fn();
const mockCreateErrorNotice = jest.fn();

beforeEach( () => {
jest.clearAllMocks();
useEntityRecords.mockReturnValue( {
records: [],
isResolving: false,
hasResolved: false,
} );
useCreateOverlayTemplatePart.mockReturnValue(
mockCreateOverlayTemplatePart
);
// Mock useDispatch to return createErrorNotice for noticesStore
useDispatch.mockReturnValue( {
createErrorNotice: mockCreateErrorNotice,
} );
} );

describe( 'Loading state', () => {
Expand Down Expand Up @@ -399,6 +430,9 @@ describe( 'OverlayTemplatePartSelector', () => {
expect(
screen.getByText( 'No overlays found.' )
).toBeInTheDocument();
expect(
screen.getByRole( 'button', { name: 'Create new?' } )
).toBeInTheDocument();
} );

it( 'should show default help text when overlays are available', () => {
Expand All @@ -415,6 +449,98 @@ describe( 'OverlayTemplatePartSelector', () => {
'Select an overlay to use for the navigation.'
)
).toBeInTheDocument();
expect(
screen.getByRole( 'button', { name: 'Create new?' } )
).toBeInTheDocument();
} );
} );

describe( 'Create overlay', () => {
it( 'should call createOverlayTemplatePart when create button is clicked', async () => {
const user = userEvent.setup();
const newOverlay = {
id: 'twentytwentyfive//overlay',
theme: 'twentytwentyfive',
slug: 'overlay',
title: {
rendered: 'Overlay',
},
area: 'overlay',
};

mockCreateOverlayTemplatePart.mockResolvedValue( newOverlay );

useEntityRecords.mockReturnValue( {
records: [],
isResolving: false,
hasResolved: true,
} );

render( <OverlayTemplatePartSelector { ...defaultProps } /> );

const createButton = screen.getByRole( 'button', {
name: 'Create new?',
} );

await user.click( createButton );

expect( mockCreateOverlayTemplatePart ).toHaveBeenCalled();
expect( mockSetAttributes ).toHaveBeenCalledWith( {
overlay: 'twentytwentyfive//overlay',
} );
expect( mockOnNavigateToEntityRecord ).toHaveBeenCalledWith( {
postId: 'twentytwentyfive//overlay',
postType: 'wp_template_part',
} );
} );

it( 'should show error notice when creation fails', async () => {
const user = userEvent.setup();

const error = new Error( 'Failed to create overlay' );
error.code = 'create_error';

mockCreateOverlayTemplatePart.mockRejectedValue( error );

useEntityRecords.mockReturnValue( {
records: [],
isResolving: false,
hasResolved: true,
} );

render( <OverlayTemplatePartSelector { ...defaultProps } /> );

const createButton = screen.getByRole( 'button', {
name: 'Create new?',
} );

await user.click( createButton );

// Wait for async operations
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );

expect( mockCreateErrorNotice ).toHaveBeenCalledWith(
'Failed to create overlay',
{ type: 'snackbar' }
);
expect( mockSetAttributes ).not.toHaveBeenCalled();
expect( mockOnNavigateToEntityRecord ).not.toHaveBeenCalled();
} );

it( 'should disable create button when overlays are resolving', () => {
useEntityRecords.mockReturnValue( {
records: [],
isResolving: true,
hasResolved: false,
} );

render( <OverlayTemplatePartSelector { ...defaultProps } /> );

const createButton = screen.getByRole( 'button', {
name: 'Create new?',
} );

expect( createButton ).toHaveAttribute( 'aria-disabled', 'true' );
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getUniqueTemplatePartTitle, getCleanTemplatePartSlug } from './utils';

/**
* Hook to create a new overlay template part.
*
* @param {Array} overlayTemplateParts Array of existing overlay template parts.
* @return {Function} Function to create a new overlay template part.
*/
export default function useCreateOverlayTemplatePart( overlayTemplateParts ) {
const { saveEntityRecord } = useDispatch( coreStore );

const createOverlayTemplatePart = useCallback( async () => {
// Generate unique name using only overlay area template parts
// Filter to only include template parts with titles for uniqueness check
const templatePartsWithTitles = overlayTemplateParts.filter(
( templatePart ) => templatePart.title?.rendered
);
const uniqueTitle = getUniqueTemplatePartTitle(
__( 'Overlay' ),
templatePartsWithTitles
);
const cleanSlug = getCleanTemplatePartSlug( uniqueTitle );

// Create the template part
const templatePart = await saveEntityRecord(
'postType',
'wp_template_part',
{
slug: cleanSlug,
title: uniqueTitle,
area: 'overlay',
},
{ throwOnError: true }
);

return templatePart;
}, [ overlayTemplateParts, saveEntityRecord ] );

return createOverlayTemplatePart;
}