UNPKG

@wordpress/block-library

Version:
317 lines (267 loc) 8.93 kB
import { act, fireEvent, render, screen, within } from '@testing-library/react'; import { useDispatch } from '@wordpress/data'; import PlaylistTrackEdit from '../edit'; import { PlaylistContext } from '../../playlist/context'; import { useUploadMediaFromBlobURL } from '../../utils/hooks'; let mockMediaReplaceFlowProps; jest.mock( '@wordpress/block-editor', () => { const PlainText = jest.requireActual( '../../../../block-editor/src/components/plain-text' ).default; return { BlockControls: ( { children } ) => <div>{ children }</div>, BlockIcon: () => <span />, InspectorControls: ( { children } ) => <div>{ children }</div>, MediaPlaceholder: () => <div />, MediaReplaceFlow: ( props ) => { mockMediaReplaceFlowProps = props; const { name, onSelect } = props; return <button onClick={ () => onSelect( {} ) }>{ name }</button>; }, MediaUpload: ( { render: renderMediaUpload } ) => renderMediaUpload( { open: jest.fn() } ), MediaUploadCheck: ( { children } ) => <div>{ children }</div>, PlainText, useBlockProps: jest.fn( () => ( {} ) ), }; } ); jest.mock( '@wordpress/data', () => { const data = jest.requireActual( '@wordpress/data' ); const mockUseDispatch = jest.fn(); return new Proxy( data, { get( target, property ) { if ( property === 'useDispatch' ) { return mockUseDispatch; } return target[ property ]; }, } ); } ); jest.mock( '@wordpress/notices', () => ( { store: 'core/notices', } ) ); jest.mock( '../../utils/hooks', () => ( { useUploadMediaFromBlobURL: jest.fn(), } ) ); // The handler only forwards the message to a notice, so its text is // irrelevant to what these tests check. const UPLOAD_ERROR = 'Upload failed.'; const defaultAttributes = { id: 1, src: 'https://example.com/song.mp3', album: 'Great Album', artist: 'The Artist', image: 'https://example.com/cover.jpg', imageAlt: 'A bright abstract track image', length: '3:45', title: 'Song One', }; function renderEdit( props = {} ) { const setAttributes = jest.fn(); const setCurrentTrackClientId = props.setCurrentTrackClientId || jest.fn(); const removeTrack = props.removeTrack || jest.fn(); render( <PlaylistContext.Provider value={ { currentTrackClientId: props.currentTrackClientId ?? null, setCurrentTrackClientId, removeTrack, } } > <PlaylistTrackEdit attributes={ { ...defaultAttributes, ...props.attributes, } } setAttributes={ setAttributes } context={ { showArtists: true, showImages: true, ...props.context, } } clientId={ props.clientId || 'playlist-track-client-id' } isSelected={ props.isSelected ?? false } /> </PlaylistContext.Provider> ); return { setAttributes, setCurrentTrackClientId, removeTrack }; } describe( 'PlaylistTrackEdit', () => { beforeEach( () => { mockMediaReplaceFlowProps = undefined; useDispatch.mockReturnValue( { createErrorNotice: jest.fn(), } ); useUploadMediaFromBlobURL.mockClear(); } ); it( 'shows the title control for a single selected playlist track', () => { renderEdit( { isSelected: true } ); expect( screen.getByRole( 'textbox', { name: 'Title' } ) ).toBeInTheDocument(); } ); it( 'shows the replace track control for a single selected playlist track', () => { renderEdit( { isSelected: true } ); expect( mockMediaReplaceFlowProps ).toBeDefined(); } ); it( 'does not show the replace track control when multiple playlist tracks are selected', () => { renderEdit( { isSelected: false } ); expect( mockMediaReplaceFlowProps ).toBeUndefined(); } ); it( 'does not show the title control when multiple playlist tracks are selected', () => { // The track inspector only renders for the selected block, or for the // first block of a same-type multi-selection, where `isSelected` is // false. renderEdit( { isSelected: false } ); expect( screen.queryByRole( 'textbox', { name: 'Title' } ) ).not.toBeInTheDocument(); expect( screen.getByRole( 'textbox', { name: 'Artist' } ) ).toBeInTheDocument(); expect( screen.getByRole( 'textbox', { name: 'Album' } ) ).toBeInTheDocument(); } ); it( 'allows the track image alternative text to be edited', () => { const { setAttributes } = renderEdit(); expect( screen.getByRole( 'link', { name: /Describe the purpose of the image\./, } ) ).toHaveAttribute( 'href', 'https://www.w3.org/WAI/tutorials/images/decision-tree/' ); expect( screen.queryByText( 'Leave empty if decorative.' ) ).not.toBeInTheDocument(); fireEvent.change( screen.getByLabelText( 'Alternative text' ), { target: { value: 'A silver microphone on a red background' }, } ); expect( setAttributes ).toHaveBeenCalledWith( { imageAlt: 'A silver microphone on a red background', } ); } ); it( 'does not show the alternative text control without a track image', () => { renderEdit( { attributes: { image: undefined, imageAlt: undefined, }, } ); expect( screen.queryByLabelText( 'Alternative text' ) ).not.toBeInTheDocument(); } ); it( 'sets the selected track as the current track', () => { const { setCurrentTrackClientId } = renderEdit( { currentTrackClientId: 'another-track-client-id', isSelected: true, } ); expect( setCurrentTrackClientId ).toHaveBeenCalledWith( 'playlist-track-client-id' ); } ); it( 'does not set a selected placeholder track as the current track', () => { const { setCurrentTrackClientId } = renderEdit( { attributes: { blob: undefined, src: undefined, }, currentTrackClientId: 'another-track-client-id', isSelected: true, } ); expect( setCurrentTrackClientId ).not.toHaveBeenCalled(); } ); it( 'uploads temporary blob tracks', () => { renderEdit( { attributes: { blob: 'blob:https://example.com/temporary-track', length: undefined, src: undefined, }, } ); expect( useUploadMediaFromBlobURL ).toHaveBeenCalledWith( expect.objectContaining( { url: 'blob:https://example.com/temporary-track', } ) ); const trackButton = screen.getByRole( 'button', { name: /Song One/, } ); expect( within( trackButton ).getByRole( 'presentation', { hidden: true } ) ).toBeInTheDocument(); } ); it( 'removes the track when its initial upload fails', () => { const createErrorNotice = jest.fn(); const removeBlock = jest.fn(); useDispatch.mockReturnValue( { createErrorNotice, removeBlock } ); const { removeTrack } = renderEdit( { attributes: { blob: 'blob:https://example.com/temporary-track', id: undefined, length: undefined, src: undefined, }, clientId: 'temporary-track', } ); const { onError } = useUploadMediaFromBlobURL.mock.calls[ 0 ][ 0 ]; act( () => { onError( UPLOAD_ERROR ); } ); expect( createErrorNotice ).toHaveBeenCalled(); // The playlist owns its inner blocks: removing the track directly // would take an unmodified playlist with it. expect( removeTrack ).toHaveBeenCalledWith( 'temporary-track' ); expect( removeBlock ).not.toHaveBeenCalled(); } ); // `onUploadError` also fires when replacing an existing track's media, so // removing the block unconditionally would discard the original track. it( 'keeps a track that already has a source when an upload fails', () => { const createErrorNotice = jest.fn(); const removeBlock = jest.fn(); useDispatch.mockReturnValue( { createErrorNotice, removeBlock } ); const { setAttributes } = renderEdit( { clientId: 'existing-track', isSelected: true, } ); // A failed replacement is reported by MediaReplaceFlow, not the // blob uploader, which never runs for a track that has a source. act( () => { mockMediaReplaceFlowProps.onError( UPLOAD_ERROR ); } ); expect( createErrorNotice ).toHaveBeenCalled(); expect( removeBlock ).not.toHaveBeenCalled(); // The original track is left untouched, so its source still plays. expect( setAttributes ).not.toHaveBeenCalled(); expect( screen.getByRole( 'button', { name: /Song One/ } ) ).toBeInTheDocument(); } ); it( 'preserves the current track source when a replacement upload fails', () => { const { setAttributes } = renderEdit( { isSelected: true } ); mockMediaReplaceFlowProps.onSelect(); expect( setAttributes ).toHaveBeenCalledTimes( 1 ); expect( setAttributes.mock.calls[ 0 ][ 0 ] ).not.toHaveProperty( 'src' ); } ); it( 'accepts raw uploaded attachment data when replacing a track', () => { const { setAttributes } = renderEdit( { isSelected: true } ); mockMediaReplaceFlowProps.onSelect( { id: 2, source_url: 'https://example.com/replacement.mp3', title: { raw: 'Replacement &amp; Track' }, } ); expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { blob: undefined, id: 2, src: 'https://example.com/replacement.mp3', title: 'Replacement & Track', } ) ); } ); } );