Skip to content

Commit

Permalink
Footnotes: Add some test coverage for footnotes logic in useEntityBlo…
Browse files Browse the repository at this point in the history
…ckEditor
  • Loading branch information
andrewserong committed Aug 4, 2023
1 parent 339cf1a commit d8f5fbc
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 2 deletions.
4 changes: 2 additions & 2 deletions packages/core-data/src/entity-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 );

Expand Down
272 changes: 272 additions & 0 deletions packages/core-data/src/test/entity-provider.js
Original file line number Diff line number Diff line change
@@ -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: '<!-- wp:test-block-with-array-of-strings --><div><p>apples</p><p></p><p>oranges</p></div><!-- /wp:test-block-with-array-of-strings --><!-- wp:test-block --><p>A paragraph</p><!-- /wp:test-block -->',
rendered: '<p>A paragraph</p>',
},
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 (
<p { ...useBlockProps.save() }>
<RichText.Content value={ content } />
</p>
);
},
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 (
<div>
{ items.map( ( item, index ) => (
<p key={ index }>{ item }</p>
) ) }
</div>
);
},
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 <div />;
};

render(
<RegistryProvider value={ registry }>
<TestComponent />
</RegistryProvider>
);

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:
'<p><sup data-fn="88e603f5-88f2-4870-b3e0-6a086b8bd566" class="fn"><a href="#88e603f5-88f2-4870-b3e0-6a086b8bd566" id="88e603f5-88f2-4870-b3e0-6a086b8bd566-link">1</a></sup></p>',
} ),
],
{
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: '<!-- wp:test-block --><p>A paragraph<sup data-fn="abcd" class="fn"><a href="#abcd" id="abcd-link">1</a></sup></p><!-- /wp:test-block -->',
rendered: '<p>A paragraph</p>',
},
meta: {
footnotes: '[]',
},
},
] );

let blocks, onChange;

const TestComponent = () => {
[ blocks, , onChange ] = useEntityBlockEditor( 'postType', 'post', {
id: 1,
} );

return <div />;
};

render(
<RegistryProvider value={ registry }>
<TestComponent />
</RegistryProvider>
);

// The first block should have the footnote number 1.
expect( blocks[ 0 ].attributes.content ).toEqual(
'A paragraph<sup data-fn="abcd" class="fn"><a href="#abcd" id="abcd-link">1</a></sup>'
);

// 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 paragraph<sup data-fn="xyz" class="xyz"><a href="#xyz" id="xyz-link">999</a></sup>',
} ),
...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 paragraph<sup data-fn="xyz" class="xyz"><a href="#xyz" id="xyz-link">1</a></sup>'
);
expect( blocks[ 1 ].attributes.content ).toEqual(
'A paragraph<sup data-fn="abcd" class="fn"><a href="#abcd" id="abcd-link">2</a></sup>'
);
} );
} );

0 comments on commit d8f5fbc

Please sign in to comment.