UNPKG

@wordpress/block-library

Version:
560 lines (459 loc) 14.3 kB
/** * External dependencies */ import { render, screen } from '@testing-library/react'; 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', () => ( { useEntityRecords: jest.fn(), store: {}, } ) ); // Mock useCreateOverlayTemplatePart hook jest.mock( '../use-create-overlay', () => ( { __esModule: true, default: jest.fn(), } ) ); // Mock useDispatch and useSelect specifically to avoid needing to set up full data store jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn(), useSelect: 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(); const defaultProps = { overlay: undefined, setAttributes: mockSetAttributes, onNavigateToEntityRecord: mockOnNavigateToEntityRecord, }; const templatePart1 = { id: 1, theme: 'twentytwentyfive', slug: 'my-overlay', title: { rendered: 'My Overlay', }, area: 'navigation-overlay', }; const templatePart2 = { id: 2, theme: 'twentytwentyfive', slug: 'another-overlay', title: { rendered: 'Another Overlay', }, area: 'navigation-overlay', }; const templatePartOtherArea = { id: 3, theme: 'twentytwentyfive', slug: 'header-part', title: { rendered: 'Header Part', }, area: 'header', }; const allTemplateParts = [ templatePart1, templatePart2, templatePartOtherArea, ]; describe( 'OverlayTemplatePartSelector', () => { const mockCreateOverlayTemplatePart = jest.fn(); const mockCreateErrorNotice = jest.fn(); const { useSelect } = require( '@wordpress/data' ); beforeEach( () => { jest.clearAllMocks(); useEntityRecords.mockReturnValue( { records: [], isResolving: false, hasResolved: false, } ); useCreateOverlayTemplatePart.mockReturnValue( mockCreateOverlayTemplatePart ); // Mock useDispatch to return createErrorNotice for noticesStore useDispatch.mockReturnValue( { createErrorNotice: mockCreateErrorNotice, } ); // Mock useSelect to return current theme // The component calls: select( coreStore ).getCurrentTheme()?.stylesheet useSelect.mockReturnValue( 'twentytwentyfive' ); } ); describe( 'Loading state', () => { it( 'should disable select control when template parts are resolving', () => { useEntityRecords.mockReturnValue( { records: null, isResolving: true, hasResolved: false, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); expect( select ).toBeDisabled(); } ); } ); describe( 'Overlay selection', () => { it( 'should show selector with "None (default)" option when no overlays are available', () => { useEntityRecords.mockReturnValue( { records: [], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); expect( select ).toBeInTheDocument(); expect( select ).toHaveValue( '' ); // Check for "None (default)" option expect( screen.getByRole( 'option', { name: 'None (default)' } ) ).toBeInTheDocument(); } ); it( 'should only show overlay (template parts) in the selector', () => { useEntityRecords.mockReturnValue( { records: allTemplateParts, isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); screen.getByRole( 'combobox', { name: 'Overlay template', } ); // Should have None + 2 overlays (not the header one) const options = screen.getAllByRole( 'option' ); expect( options ).toHaveLength( 3 ); // None + 2 overlay parts expect( screen.getByRole( 'option', { name: 'My Overlay' } ) ).toBeInTheDocument(); expect( screen.getByRole( 'option', { name: 'Another Overlay' } ) ).toBeInTheDocument(); expect( screen.queryByRole( 'option', { name: 'Header Part' } ) ).not.toBeInTheDocument(); } ); it( 'should display overlay slug when title is missing', () => { const templatePartNoTitle = { ...templatePart1, title: null, }; useEntityRecords.mockReturnValue( { records: [ templatePartNoTitle ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); expect( screen.getByRole( 'option', { name: 'my-overlay' } ) ).toBeInTheDocument(); } ); it( 'should store slug only when an overlay is selected', async () => { const user = userEvent.setup(); useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); await user.selectOptions( select, 'my-overlay' ); expect( mockSetAttributes ).toHaveBeenCalledWith( { overlay: 'my-overlay', } ); } ); it( 'unsets custom overlay when "None (default)" is selected', async () => { const user = userEvent.setup(); useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" /> ); const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); await user.selectOptions( select, '' ); expect( mockSetAttributes ).toHaveBeenCalledWith( { overlay: undefined, } ); } ); it( 'should display selected overlay by slug', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" /> ); const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); expect( select ).toHaveValue( 'my-overlay' ); } ); } ); describe( 'Edit button', () => { it( 'should not render when no overlay is selected', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); const editButton = screen.queryByRole( 'button', { name: 'Edit overlay', } ); expect( editButton ).not.toBeInTheDocument(); } ); it( 'should not display edit button while overlays templates are loading', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: true, hasResolved: false, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" /> ); // Component shows disabled select and disabled button when loading const select = screen.getByRole( 'combobox', { name: 'Overlay template', } ); expect( select ).toBeDisabled(); // Expect Edit button to not be in the document expect( screen.queryByRole( 'button', { name: ( accessibleName ) => accessibleName.startsWith( 'Edit overlay' ), } ) ).not.toBeInTheDocument(); } ); it( 'should be enabled when a valid overlay is selected', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" /> ); const editButton = screen.getByRole( 'button', { name: ( accessibleName ) => accessibleName.startsWith( 'Edit overlay' ), } ); expect( editButton ).toBeEnabled(); expect( editButton ).toHaveAccessibleName(); } ); it( 'should be disabled when navigation to focused overlay editor is not available', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" onNavigateToEntityRecord={ undefined } /> ); const editButton = screen.getByRole( 'button', { name: ( accessibleName ) => accessibleName.startsWith( 'Edit overlay' ), } ); // Button uses accessibleWhenDisabled, so it has aria-disabled instead of disabled expect( editButton ).toHaveAttribute( 'aria-disabled', 'true' ); } ); it( 'should navigate to focused overlay editor with full ID when edit button is clicked', async () => { const user = userEvent.setup(); useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" /> ); const editButton = screen.getByRole( 'button', { name: ( accessibleName ) => accessibleName.startsWith( 'Edit overlay' ), } ); await user.click( editButton ); // Should construct full ID from theme and slug expect( mockOnNavigateToEntityRecord ).toHaveBeenCalledWith( { postId: 'twentytwentyfive//my-overlay', postType: 'wp_template_part', } ); } ); it( 'should not navigate to focused overlay editor when button is disabled', async () => { const user = userEvent.setup(); useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } overlay="my-overlay" onNavigateToEntityRecord={ undefined } /> ); const editButton = screen.getByRole( 'button', { name: ( accessibleName ) => accessibleName.startsWith( 'Edit overlay' ), } ); // Button uses accessibleWhenDisabled, so it has aria-disabled instead of disabled expect( editButton ).toHaveAttribute( 'aria-disabled', 'true' ); // Even if clicked, the handler checks for onNavigateToEntityRecord and won't call it await user.click( editButton ); expect( mockOnNavigateToEntityRecord ).not.toHaveBeenCalled(); } ); } ); describe( 'Help text', () => { it( 'should show help text when no overlays are available', () => { useEntityRecords.mockReturnValue( { records: [], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); expect( screen.getByText( 'No overlays found.' ) ).toBeInTheDocument(); expect( screen.getByRole( 'button', { name: 'Create new overlay template', } ) ).toBeInTheDocument(); } ); it( 'should show default help text when overlays are available', () => { useEntityRecords.mockReturnValue( { records: [ templatePart1 ], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); expect( screen.getByText( 'Select an overlay for navigation.' ) ).toBeInTheDocument(); expect( screen.getByRole( 'button', { name: 'Create new overlay template', } ) ).toBeInTheDocument(); } ); } ); describe( 'Create overlay', () => { it( 'should store slug only and navigate with full ID when creating overlay', async () => { const user = userEvent.setup(); const newOverlay = { id: 'twentytwentyfive//overlay', theme: 'twentytwentyfive', slug: 'overlay', title: { rendered: 'Overlay', }, area: 'navigation-overlay', }; mockCreateOverlayTemplatePart.mockResolvedValue( newOverlay ); useEntityRecords.mockReturnValue( { records: [], isResolving: false, hasResolved: true, } ); render( <OverlayTemplatePartSelector { ...defaultProps } /> ); const createButton = screen.getByRole( 'button', { name: 'Create new overlay template', } ); await user.click( createButton ); expect( mockCreateOverlayTemplatePart ).toHaveBeenCalled(); // Should store slug only expect( mockSetAttributes ).toHaveBeenCalledWith( { overlay: 'overlay', } ); // Should navigate with full ID constructed from theme and slug 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 overlay template', } ); 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 overlay template', } ); expect( createButton ).toHaveAttribute( 'aria-disabled', 'true' ); } ); } ); } );