@wordpress/block-library
Version:
Block library for the WordPress editor.
634 lines (557 loc) • 18 kB
JavaScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { userEvent } from 'vitest/browser';
import {
cleanup as cleanupBrowser,
render as renderBrowser,
} from 'vitest-browser-react';
import { screen, act, within, waitFor } from '@testing-library/react';
import {
createBlock,
getBlockTypes,
unregisterBlockType,
} from '@wordpress/blocks';
import { createElement, useState } from '@wordpress/element';
import { dispatch, select } from '@wordpress/data';
import {
store as richTextStore,
unregisterFormatType,
} from '@wordpress/rich-text';
import {
BlockInspector,
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
initializeEditor,
selectBlock,
} from '@wordpress/integration-tests/helpers/integration-test-editor';
import { registerCoreBlocks } from '@wordpress/block-library';
// Keep the matrix popover above the block toolbar, as in the editor.
import '../../../../block-editor/src/components/block-popover/style.scss';
import '../../../../components/src/popover/style.scss';
import { unlock } from '../../lock-unlock';
import { getMediaColor } from '../edit/color-utils';
vi.mock( import( '../edit/color-utils' ), { spy: true } );
const {
ExperimentalBlockCanvas: BlockCanvas,
ExperimentalBlockEditorProvider: BlockEditorProvider,
openMediaEditorModalKey,
} = unlock( blockEditorPrivateApis );
const defaultSettings = {
__experimentalFeatures: {
color: {
defaultPalette: true,
defaultGradients: true,
palette: {
default: [
{ name: 'Black', slug: 'black', color: '#000000' },
{ name: 'White', slug: 'white', color: '#ffffff' },
],
},
},
},
colors: [ { name: 'Black', slug: 'black', color: '#000000' } ],
disableCustomColors: false,
disableCustomGradients: false,
};
const disabledColorSettings = {
color: {
defaultPalette: false,
defaultGradients: false,
},
disableCustomColors: true,
disableCustomGradients: true,
};
async function setup( attributes, customSettings, innerBlocks = [] ) {
const testBlock = { name: 'core/cover', attributes, innerBlocks };
const settings = customSettings || defaultSettings;
return initializeEditor( testBlock, false, settings );
}
async function activateWithKeyboard( element ) {
act( () => element.focus() );
expect( element ).toHaveFocus();
await userEvent.keyboard( '{Enter}' );
}
async function createAndSelectBlock() {
const view = await setup( { overlayColor: 'black' }, undefined, [
createBlock( 'core/paragraph' ),
] );
const cover = screen.getByLabelText( 'Block: Cover' );
await act( async () => cover.focus() );
await waitFor( () => expect( cover ).toHaveClass( 'is-selected' ) );
return view;
}
async function openStylesTabIfAvailable() {
const stylesTab = screen.queryByRole( 'tab', {
name: 'Styles',
} );
if ( stylesTab ) {
await userEvent.click( stylesTab );
}
}
async function openSettingsTabIfAvailable() {
const settingsTab = screen.queryByRole( 'tab', {
name: 'Settings',
} );
if ( settingsTab ) {
await userEvent.click( settingsTab );
}
}
describe( 'Cover block', () => {
beforeEach( () => registerCoreBlocks() );
describe( 'Editor canvas', () => {
test( 'shows placeholder if background image and color not set', async () => {
await setup();
expect(
within( screen.getByLabelText( 'Block: Cover' ) ).getByText(
'To edit this block, you need permission to upload media.'
)
).toBeInTheDocument();
} );
test( 'can set overlay color using color picker on block placeholder', async () => {
await setup();
const colorPicker = screen.getByRole( 'button', {
name: 'Black',
} );
await activateWithKeyboard( colorPicker );
const color = colorPicker.style.backgroundColor;
await waitFor( () =>
expect(
screen.getByLabelText( 'Block: Cover' )
).not.toHaveClass( 'is-placeholder' )
);
const coverBlock = screen.getByLabelText( 'Block: Cover' );
// eslint-disable-next-line testing-library/no-node-access
const overlay = coverBlock.querySelector(
'.wp-block-cover__background'
);
await waitFor( () =>
expect(
window.getComputedStyle( overlay ).backgroundColor
).toBe( color )
);
} );
test( 'can have the title edited', async () => {
await setup();
const colorButton = screen.getByRole( 'button', { name: 'Black' } );
await activateWithKeyboard( colorButton );
await waitFor( () =>
expect(
screen.getByLabelText( 'Block: Cover' )
).not.toHaveClass( 'is-placeholder' )
);
const title = screen.getByLabelText( 'Empty block;', {
exact: false,
} );
act( () => title.focus() );
expect( title ).toHaveFocus();
await userEvent.keyboard( 'abc' );
expect( title ).toHaveTextContent( 'abc' );
} );
} );
describe( 'Block toolbar', () => {
test( 'full height toggle sets minHeight style attribute to 100vh when clicked', async () => {
await createAndSelectBlock();
const cover = screen.getByLabelText( 'Block: Cover' );
expect( window.getComputedStyle( cover ).minHeight ).not.toBe(
`${ window.innerHeight }px`
);
await userEvent.click( screen.getByLabelText( 'Full height' ) );
await waitFor( () =>
expect(
window.getComputedStyle(
screen.getByLabelText( 'Block: Cover' )
).minHeight
).toBe( `${ window.innerHeight }px` )
);
} );
test( 'content position button sets content position', async () => {
await createAndSelectBlock();
const cover = screen.getByLabelText( 'Block: Cover' );
expect( cover ).not.toHaveClass( 'has-custom-content-position' );
await userEvent.click(
screen.getByLabelText( 'Change content position' )
);
expect( cover ).not.toHaveClass( 'has-custom-content-position' );
await userEvent.click(
within( screen.getByRole( 'grid' ) ).getByRole( 'gridcell', {
name: 'top left',
} )
);
await waitFor( () =>
expect( cover ).toHaveClass( 'has-custom-content-position' )
);
expect( cover ).toHaveClass( 'is-position-top-left' );
} );
test( 'clears media when clear media button clicked', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
await selectBlock( 'Block: Cover' );
expect(
within( screen.getByLabelText( 'Block: Cover' ) ).getByRole(
'presentation'
)
).toBeInTheDocument();
await userEvent.click(
screen.getByRole( 'button', { name: 'Replace' } )
);
await activateWithKeyboard(
screen.getByRole( 'menuitem', { name: 'Reset' } )
);
expect(
within( screen.getByLabelText( 'Block: Cover' ) ).queryByRole(
'presentation'
)
).not.toBeInTheDocument();
} );
} );
describe( 'media editor Undo', () => {
const originalUrl =
'data:image/svg+xml,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="black"/></svg>'
);
const updatedImage = { id: 11, url: `${ originalUrl }#edited` };
beforeEach( () => {
vi.mocked( getMediaColor ).mockResolvedValue( '#ffffff' );
} );
afterEach( async () => {
await cleanupBrowser();
getBlockTypes().forEach( ( { name } ) =>
unregisterBlockType( name )
);
select( richTextStore )
.getFormatTypes()
.forEach( ( { name } ) => unregisterFormatType( name ) );
} );
async function openMediaEditor( initialAttributes = {} ) {
const openModal = vi.fn();
const block = createBlock( 'core/cover', {
id: 10,
url: originalUrl,
backgroundType: 'image',
sizeSlug: 'medium',
overlayColor: 'black',
isDark: true,
isUserOverlayColor: false,
...initialAttributes,
} );
function TestEditor() {
const [ blocks, setBlocks ] = useState( [ block ] );
return createElement(
BlockEditorProvider,
{
value: blocks,
onInput: setBlocks,
onChange: setBlocks,
useSubRegistry: false,
settings: {
...defaultSettings,
[ openMediaEditorModalKey ]: openModal,
},
},
createElement( BlockInspector ),
createElement( BlockCanvas, {
height: '100%',
shouldIframe: false,
} )
);
}
await renderBrowser( createElement( TestEditor ) );
await userEvent.click( screen.getByLabelText( 'Block: Cover' ) );
await userEvent.click(
screen.getByRole( 'button', { name: 'Edit image' } )
);
const { clientId } = select( blockEditorStore ).getBlocks()[ 0 ];
return {
...openModal.mock.calls[ 0 ][ 0 ],
getAttributes: () =>
select( blockEditorStore ).getBlockAttributes( clientId ),
editAttributes: ( attributes ) =>
dispatch( blockEditorStore ).updateBlockAttributes(
clientId,
attributes
),
};
}
test( 'restores only the previous image without reverting Cover settings', async () => {
const { onUpdate, onUndo, getAttributes, editAttributes } =
await openMediaEditor();
await act( () => onUpdate( updatedImage ) );
expect( getAttributes() ).toMatchObject( {
id: 11,
sizeSlug: 'full',
overlayColor: 'white',
isDark: false,
} );
await act( () =>
editAttributes( { dimRatio: 75, sizeSlug: 'large' } )
);
const beforeUndo = getAttributes();
act( () => onUndo() );
expect( getAttributes() ).toEqual( {
...beforeUndo,
id: 10,
url: originalUrl,
} );
} );
test( 'preserves an overlay chosen after saving when undoing the media update', async () => {
const { onUpdate, onUndo, getAttributes, editAttributes } =
await openMediaEditor();
await act( () => onUpdate( updatedImage ) );
await act( () =>
editAttributes( {
overlayColor: undefined,
customOverlayColor: '#000000',
isUserOverlayColor: true,
dimRatio: 90,
isDark: true,
} )
);
const beforeUndo = getAttributes();
act( () => onUndo() );
expect( getAttributes() ).toEqual( {
...beforeUndo,
id: 10,
url: originalUrl,
} );
} );
test( 'does not reapply a media update when Undo runs before its color loads', async () => {
const { onUpdate, onUndo, getAttributes } = await openMediaEditor();
const previous = getAttributes();
let resolveColor;
vi.mocked( getMediaColor ).mockReturnValueOnce(
new Promise( ( resolve ) => {
resolveColor = resolve;
} )
);
let updatePromise;
act( () => {
updatePromise = onUpdate( updatedImage );
} );
act( () => onUndo() );
await act( async () => {
resolveColor( '#ffffff' );
await updatePromise;
} );
expect( getAttributes() ).toEqual( previous );
expect(
screen.getByRole( 'button', { name: 'Edit image' } )
).toBeEnabled();
} );
} );
describe( 'Inspector controls', () => {
describe( 'Media settings', () => {
test( 'does not display media settings panel if url is not set', async () => {
await setup();
expect(
screen.queryByRole( 'heading', {
name: 'Settings',
} )
).not.toBeInTheDocument();
} );
test( 'does not display settings panel in the settings tab when the block has no media', async () => {
await createAndSelectBlock();
await openSettingsTabIfAvailable();
expect(
screen.queryByRole( 'heading', {
name: 'Settings',
} )
).not.toBeInTheDocument();
expect(
screen.getByRole( 'button', {
name: 'Advanced',
} )
).toBeInTheDocument();
} );
test( 'displays media settings panel if url is set', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
await selectBlock( 'Block: Cover' );
await openSettingsTabIfAvailable();
expect(
await screen.findByRole( 'heading', { name: 'Settings' } )
).toBeInTheDocument();
} );
} );
test( 'sets hasParallax attribute to true if fixed background toggled', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass(
'has-parallax'
);
await selectBlock( 'Block: Cover' );
await openSettingsTabIfAvailable();
await userEvent.click(
await screen.findByLabelText( 'Fixed background' )
);
expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
'has-parallax'
);
} );
test( 'sets isRepeated attribute to true if repeated background toggled', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass(
'is-repeated'
);
await selectBlock( 'Block: Cover' );
await openSettingsTabIfAvailable();
await userEvent.click(
await screen.findByLabelText( 'Repeated background' )
);
expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
'is-repeated'
);
} );
test( 'sets left focalPoint attribute when focal point values changed', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
await selectBlock( 'Block: Cover' );
await openSettingsTabIfAvailable();
await userEvent.clear( await screen.findByLabelText( 'Left' ) );
await userEvent.type( screen.getByLabelText( 'Left' ), '100' );
const image = within(
screen.getByLabelText( 'Block: Cover' )
).getByRole( 'presentation' );
expect( window.getComputedStyle( image ).objectPosition ).toBe(
'100% 50%'
);
} );
test( 'sets alt attribute if text entered in alt text box', async () => {
await setup( {
url: 'http://localhost/my-image.jpg',
} );
await selectBlock( 'Block: Cover' );
await openSettingsTabIfAvailable();
await userEvent.type(
await screen.findByLabelText( 'Alternative text' ),
'Me'
);
expect( screen.getByAltText( 'Me' ) ).toBeInTheDocument();
} );
describe( 'Color panel', () => {
test( 'applies selected opacity to block when number control value changed', async () => {
const { container } = await createAndSelectBlock();
// eslint-disable-next-line testing-library/no-node-access
const overlay = container.getElementsByClassName(
'wp-block-cover__background'
);
expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' );
await openStylesTabIfAvailable();
await userEvent.fill(
screen.getByRole( 'spinbutton', {
name: 'Overlay opacity',
} ),
'40'
);
expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-40' );
} );
test( 'applies selected opacity to block when slider moved', async () => {
const { container } = await createAndSelectBlock();
// eslint-disable-next-line testing-library/no-node-access
const overlay = container.getElementsByClassName(
'wp-block-cover__background'
);
expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' );
await openStylesTabIfAvailable();
await userEvent.fill(
screen.getByRole( 'slider', {
name: 'Overlay opacity',
} ),
'30'
);
expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-30' );
} );
describe( 'when colors are disabled', () => {
test( 'does not render overlay control', async () => {
await setup( undefined, disabledColorSettings );
await selectBlock( 'Block: Cover' );
await openStylesTabIfAvailable();
const overlayControl = screen.queryByRole( 'button', {
name: 'Overlay',
} );
expect( overlayControl ).not.toBeInTheDocument();
} );
test( 'does not render opacity control', async () => {
await setup( undefined, disabledColorSettings );
await selectBlock( 'Block: Cover' );
await openStylesTabIfAvailable();
const opacityControl = screen.queryByRole( 'slider', {
name: 'Overlay opacity',
} );
expect( opacityControl ).not.toBeInTheDocument();
} );
} );
} );
describe( 'Dimensions panel', () => {
test( 'sets minHeight attribute when number control value changed', async () => {
await createAndSelectBlock();
await openStylesTabIfAvailable();
await userEvent.clear(
screen.getByLabelText( 'Minimum height' )
);
await userEvent.type(
screen.getByLabelText( 'Minimum height' ),
'300'
);
expect(
window.getComputedStyle(
screen.getByLabelText( 'Block: Cover' )
).minHeight
).toBe( '300px' );
} );
} );
} );
describe( 'isDark settings', () => {
test( 'should toggle is-light class if background changed from light to dark', async () => {
await setup();
const colorPicker = screen.getByRole( 'button', {
name: 'White',
} );
await activateWithKeyboard( colorPicker );
await waitFor( () =>
expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
'is-light'
)
);
const coverBlock = screen.getByLabelText( 'Block: Cover' );
await selectBlock( 'Block: Cover' );
await openStylesTabIfAvailable();
await userEvent.click( screen.getByText( 'Overlay' ) );
const popupColorPicker = screen.getByRole( 'option', {
name: 'Black',
} );
await userEvent.click( popupColorPicker );
expect( coverBlock ).not.toHaveClass( 'is-light' );
} );
test( 'should remove is-light class if overlay color is removed', async () => {
await setup();
const colorPicker = screen.getByRole( 'button', {
name: 'White',
} );
await activateWithKeyboard( colorPicker );
await waitFor( () =>
expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
'is-light'
)
);
const coverBlock = screen.getByLabelText( 'Block: Cover' );
await selectBlock( 'Block: Cover' );
await openStylesTabIfAvailable();
await userEvent.click( screen.getByText( 'Overlay' ) );
// The default color is black, so clicking the black color button will remove the background color,
// which should remove the isDark setting and assign the is-light class.
const popupColorPicker = screen.getByRole( 'option', {
name: 'White',
} );
await userEvent.click( popupColorPicker );
expect( coverBlock ).not.toHaveClass( 'is-light' );
} );
} );
} );