From d8f5fbcde12489ea1db3e83f3f7c742308ed32a8 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Fri, 4 Aug 2023 21:51:11 +1000 Subject: [PATCH 1/2] Footnotes: Add some test coverage for footnotes logic in useEntityBlockEditor --- packages/core-data/src/entity-provider.js | 4 +- .../core-data/src/test/entity-provider.js | 272 ++++++++++++++++++ 2 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 packages/core-data/src/test/entity-provider.js diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index cf15c423456a29..d32b3853627b32 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -142,7 +142,7 @@ export function useEntityProp( kind, name, prop, _id ) { * The return value has the shape `[ blocks, onInput, onChange ]`. * `onInput` is for block changes that don't create undo levels * or dirty the post, non-persistent changes, and `onChange` is for - * peristent changes. They map directly to the props of a + * persistent changes. They map directly to the props of a * `BlockEditorProvider` and are intended to be used with it, * or similar components or hooks. * @@ -290,7 +290,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { } ); } - // We need to go through all block attributs deeply and update the + // We need to go through all block attributes deeply and update the // footnote anchor numbering (textContent) to match the new order. const newBlocks = updateBlocksAttributes( _blocks ); diff --git a/packages/core-data/src/test/entity-provider.js b/packages/core-data/src/test/entity-provider.js new file mode 100644 index 00000000000000..e59aec5798541a --- /dev/null +++ b/packages/core-data/src/test/entity-provider.js @@ -0,0 +1,272 @@ +/** + * External dependencies + */ +import { act, render } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { + createBlock, + registerBlockType, + unregisterBlockType, + getBlockTypes, +} from '@wordpress/blocks'; +import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { createRegistry, RegistryProvider } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as coreDataStore } from '../index'; +import { useEntityBlockEditor } from '../entity-provider'; + +const postTypeConfig = { + kind: 'postType', + name: 'post', + baseURL: '/wp/v2/posts', + transientEdits: { blocks: true, selection: true }, + mergedEdits: { meta: true }, + rawAttributes: [ 'title', 'excerpt', 'content' ], +}; + +const postTypeEntity = { + slug: 'post', + rest_base: 'posts', + labels: { + item_updated: 'Updated Post', + item_published: 'Post published', + item_reverted_to_draft: 'Post reverted to draft.', + }, +}; + +const aSinglePost = { + id: 1, + type: 'post', + content: { + raw: '

apples

oranges

A paragraph

', + rendered: '

A paragraph

', + }, + meta: { + footnotes: '[]', + }, +}; + +function createRegistryWithStores() { + // Create a registry. + const registry = createRegistry(); + + // Register store. + registry.register( coreDataStore ); + + // Register post type entity. + registry.dispatch( coreDataStore ).addEntities( [ postTypeConfig ] ); + + // Store post type entity. + registry + .dispatch( coreDataStore ) + .receiveEntityRecords( 'root', 'postType', [ postTypeEntity ] ); + + // Store a single post for use by the tests. + registry + .dispatch( coreDataStore ) + .receiveEntityRecords( 'postType', 'post', [ aSinglePost ] ); + + return registry; +} + +describe( 'useEntityBlockEditor', () => { + let registry; + + beforeEach( () => { + registry = createRegistryWithStores(); + + const edit = ( { children } ) => <>{ children }; + + registerBlockType( 'core/test-block', { + supports: { + className: false, + }, + save: ( { attributes } ) => { + const { content } = attributes; + return ( +

+ +

+ ); + }, + category: 'text', + attributes: { + content: { + type: 'string', + source: 'html', + selector: 'p', + default: '', + __experimentalRole: 'content', + }, + }, + title: 'block title', + edit, + } ); + + registerBlockType( 'core/test-block-with-array-of-strings', { + supports: { + className: false, + }, + save: ( { attributes } ) => { + const { items } = attributes; + return ( +
+ { items.map( ( item, index ) => ( +

{ item }

+ ) ) } +
+ ); + }, + category: 'text', + attributes: { + items: { + type: 'array', + items: { + type: 'string', + }, + default: [ 'apples', null, 'oranges' ], + }, + }, + title: 'block title', + edit, + } ); + } ); + + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'does not mutate block attributes that include an array of strings or null values', async () => { + let blocks, onChange; + const TestComponent = () => { + [ blocks, , onChange ] = useEntityBlockEditor( 'postType', 'post', { + id: 1, + } ); + + return
; + }; + + render( + + + + ); + + expect( blocks[ 0 ].name ).toEqual( + 'core/test-block-with-array-of-strings' + ); + expect( blocks[ 0 ].attributes.items ).toEqual( [ + 'apples', + null, + 'oranges', + ] ); + + // Add a block with content that will match against footnotes logic, causing + // `updateFootnotes` to iterate over blocks and their attributes. + act( () => { + onChange( + [ + ...blocks, + createBlock( 'core/test-block', { + content: + '

1

', + } ), + ], + { + selection: { + selectionStart: {}, + selectionEnd: {}, + initialPosition: {}, + }, + } + ); + } ); + + // Ensure the first block remains the same, with unaltered attributes. + expect( blocks[ 0 ].name ).toEqual( + 'core/test-block-with-array-of-strings' + ); + expect( blocks[ 0 ].attributes.items ).toEqual( [ + 'apples', + null, + 'oranges', + ] ); + } ); + + it( 'updates the order of footnotes when a new footnote is inserted', async () => { + // Start with a post containing a block with a single footnote (set to 1). + registry + .dispatch( coreDataStore ) + .receiveEntityRecords( 'postType', 'post', [ + { + id: 1, + type: 'post', + content: { + raw: '

A paragraph1

', + rendered: '

A paragraph

', + }, + meta: { + footnotes: '[]', + }, + }, + ] ); + + let blocks, onChange; + + const TestComponent = () => { + [ blocks, , onChange ] = useEntityBlockEditor( 'postType', 'post', { + id: 1, + } ); + + return
; + }; + + render( + + + + ); + + // The first block should have the footnote number 1. + expect( blocks[ 0 ].attributes.content ).toEqual( + 'A paragraph1' + ); + + // Add a block with a new footnote with an arbitrary footnote number that will be overwritten after insertion. + act( () => { + onChange( + [ + createBlock( 'core/test-block', { + content: + 'A new paragraph999', + } ), + ...blocks, + ], + { + selection: { + selectionStart: {}, + selectionEnd: {}, + initialPosition: {}, + }, + } + ); + } ); + + // The newly inserted block should have the footnote number 1, and the + // existing footnote number 1 should be updated to 2. + expect( blocks[ 0 ].attributes.content ).toEqual( + 'A new paragraph1' + ); + expect( blocks[ 1 ].attributes.content ).toEqual( + 'A paragraph2' + ); + } ); +} ); From b333607663b4ec5b48b24ac9d9507bc15430e7aa Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:08:31 +1000 Subject: [PATCH 2/2] Simplify test string --- packages/core-data/src/test/entity-provider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-data/src/test/entity-provider.js b/packages/core-data/src/test/entity-provider.js index e59aec5798541a..728630b482626c 100644 --- a/packages/core-data/src/test/entity-provider.js +++ b/packages/core-data/src/test/entity-provider.js @@ -177,7 +177,7 @@ describe( 'useEntityBlockEditor', () => { ...blocks, createBlock( 'core/test-block', { content: - '

1

', + '

1

', } ), ], {