UNPKG

@wordpress/block-library

Version:
634 lines (557 loc) • 18 kB
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' ); } ); } ); } );