@wordpress/block-library
Version:
Block library for the WordPress editor.
317 lines (267 loc) • 8.93 kB
JavaScript
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 & Track' },
} );
expect( setAttributes ).toHaveBeenCalledWith(
expect.objectContaining( {
blob: undefined,
id: 2,
src: 'https://example.com/replacement.mp3',
title: 'Replacement & Track',
} )
);
} );
} );