Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Footnotes: Add some test coverage for footnotes logic in useEntityBlockEditor #53376

Merged
merged 2 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave my attributs out of it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, so rude! 😆

// 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="1234" class="fn"><a href="#1234" id="1234-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>'
);
} );
} );
Loading