From 13560d1124dba8802c5a53f480325a8a46087c37 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 6 Aug 2018 10:19:46 -0400 Subject: [PATCH] =?UTF-8?q?Parser:=20synchronous=20=E2=86=92=20asynchronou?= =?UTF-8?q?s=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this patch **we're changing the execution model of the `post_content` parser from _synchronous_ to _asynchronous_**. ```js const doc = '

Wee!

'; const parseSyncNowBroken = ( doc ) => { const parsed = parseWithGrammar( doc ); return parsed.length === 1; } const parseAsyncNowWorks = ( doc ) => { return parseWithGrammar( doc ).then( parsed => { return parsed.length === 1; } ); } const usingAsyncAwait = async ( doc ) => { const parsed = await parseWithGrammar( doc ); return parsed.length === 1; } ``` So far whenever we have relied on parsing the raw content from `post_content` we have done so synchronously and waited for the parser to finish before continuing execution. When the parse is instant and built in a synchronous manner this works well. It imposes some limitations though that we may not want. - execution flow is straightforward - loading state is "free" because nothing renders until parse finishes - execution of the remainder of the app waits for every parse - cannot run parser in `WebWorker` or other context - cannot run parsers written in an asynchronous manner These limitations are things we anticipated and even since the beginnings of the project we could assume that at some point we would want an asynchronous model. Recently @hywan wrote a fast implementation of the project's parser specification but the output depends on an asynchronous model. In other words, the timing is right for us to adopt this change. - parsing doesn't block the UI - parsing can happen in a `WebWorker`, over the network, or in any asynchronous manner - UI _must_ become async-aware, the loading state is no longer "free" - race conditions _can_ appear if not planned for and avoided Sadly once we enter an asynchronous world we invite complexities and race conditions. The code in this PR so-far doesn't address either of these. The first thing you might notice is that when loading a document in the editor we end up loading a blank document for a spit-second before we load the parsed document. If you don't see this then modify `post-parser.js` to this instead and it will become obvious… ```js import { parse as syncParse } from './post.pegjs'; export const parse = ( document ) => new Promise( ( resolve ) => { setTimeout( () => resolve( syncParse( document ) ), 2500 ); } ); ``` With this change we are simulating that it takes 2.5s to parse our document. You should see an empty document load in the editor immediately and then after the delay the parsed document will surprisingly appear. During that initial delay we can interact with the empty document and this means that we can create a state where we mess up the document and its history - someone will think they lost their post. For the current parsers this shouldn't be a practical problem since the parse is fast but likely people will see an initial flash. To mitigate this problem we need to somehow introduce a loading state for the editor: "we are in the process of loading the initial document" and that can appear as a message where the contents would otherwise reside or it could simply be a blank area deferring the render until ready. A common approach I have seen is to keep things blanked out unless the operation takes longer than a given threshold to complete so as not to jar the experience with the flashing loading message, but that's really a detail that isn't the most important thing right now. As for the race condition we may need to consider the risk and the cost of the solution. Since I think the flash would likely be more jarring than the race condition likely it may be a problem we can feasibly defer. The biggest risk is probably when we have code rapidly calling `parse()` in sequence and the results come back out of order or if they start from different copies of the original document. One way we can mitigate this is by enforcing a constraint on the parser to be a single actor and only accept (or queue up instead) parsing requests until the current work is finished. - Please review the code changes here and reflect on their implications. The tests had to change and so do all functions that rely on parsing the `post_content` - they must become asynchronous themselves. - Please consider the race conditions and the experience implications and weigh in on how severe you estimate them to be. - Please share any further thoughts or ideas you may have concerning the sync vs. async behavior. --- docs/reference/faq.md | 5 +- .../src/paragraph/edit.native.js | 4 +- packages/blocks/src/api/parser.js | 8 ++- packages/blocks/src/api/raw-handling/index.js | 10 ++-- packages/blocks/src/api/test/parser.js | 40 ++++++------- .../src/components/block-drop-zone/index.js | 4 +- .../block-list/block-invalid-warning.js | 4 +- .../block-html-convert-button.js | 4 +- .../block-unknown-convert-button.js | 4 +- .../src/components/post-text-editor/index.js | 4 +- .../editor/src/components/rich-text/index.js | 7 ++- packages/editor/src/store/effects.js | 42 +++++++------ .../src/store/effects/reusable-blocks.js | 8 +-- packages/editor/src/store/test/effects.js | 60 ++++++++++++------- test/integration/blocks-raw-handling.spec.js | 38 ++++++------ .../full-content/full-content.spec.js | 4 +- 16 files changed, 139 insertions(+), 107 deletions(-) diff --git a/docs/reference/faq.md b/docs/reference/faq.md index 9be85efa266ff..41bf207972a3a 100644 --- a/docs/reference/faq.md +++ b/docs/reference/faq.md @@ -161,10 +161,13 @@ This also [gives us the flexibility](https://github.com/WordPress/gutenberg/issu We suggest you look at the [language of Gutenberg](../../docs/language.md) to learn more about how this aspect of the project works. ## How can I parse the post content back out into blocks in PHP or JS? + In JS: ```js -var blocks = wp.blocks.parse( postContent ); +wp.blocks.parse( postContent ).then( function( blocks ) { + // ... +} ); ``` In PHP: diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 848d1dc485e5f..b9e51be9acafd 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -34,9 +34,9 @@ class ParagraphEdit extends Component { ...style, minHeight: Math.max( minHeight, typeof attributes.aztecHeight === 'undefined' ? 0 : attributes.aztecHeight ), } } - onChange={ ( event ) => { + onChange={ async ( event ) => { // Create a React Tree from the new HTML - const newParaBlock = parse( '

' + event.content + '

' )[ 0 ]; + const newParaBlock = await parse( '

' + event.content + '

' )[ 0 ]; setAttributes( { ...this.props.attributes, content: newParaBlock.attributes.content, diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index e0a3c03e9c89c..64301b6211ec4 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -357,7 +357,7 @@ export function createBlockWithFallback( blockNode ) { * @return {Function} An implementation which parses the post content. */ const createParse = ( parseImplementation ) => - ( content ) => parseImplementation( content ).reduce( ( memo, blockNode ) => { + async ( content ) => ( await parseImplementation( content ) ).reduce( ( memo, blockNode ) => { const block = createBlockWithFallback( blockNode ); if ( block ) { memo.push( block ); @@ -372,6 +372,10 @@ const createParse = ( parseImplementation ) => * * @return {Array} Block list. */ -export const parseWithGrammar = createParse( grammarParse ); +export const parseWithGrammar = createParse( ( postContent ) => { + const result = grammarParse( postContent ); + + return Promise.resolve( result ); +} ); export default parseWithGrammar; diff --git a/packages/blocks/src/api/raw-handling/index.js b/packages/blocks/src/api/raw-handling/index.js index 557cafac2db16..8500e46a6cdf0 100644 --- a/packages/blocks/src/api/raw-handling/index.js +++ b/packages/blocks/src/api/raw-handling/index.js @@ -77,7 +77,7 @@ function getRawTransformations() { * @param {Array} [options.tagName] The tag into which content will be inserted. * @param {boolean} [options.canUserUseUnfilteredHTML] Whether or not the user can use unfiltered HTML. * - * @return {Array|string} A list of blocks or a string, depending on `handlerMode`. + * @return {Promise} A list of blocks or a string, depending on `handlerMode`. */ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) { // First of all, strip any meta tags. @@ -121,7 +121,7 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', } if ( mode === 'INLINE' ) { - return filterInlineHTML( HTML ); + return Promise.resolve( filterInlineHTML( HTML ) ); } // An array of HTML strings and block objects. The blocks replace matched @@ -134,14 +134,14 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', const hasShortcodes = pieces.length > 1; if ( mode === 'AUTO' && ! hasShortcodes && isInlineContent( HTML, tagName ) ) { - return filterInlineHTML( HTML ); + return Promise.resolve( filterInlineHTML( HTML ) ); } const rawTransformations = getRawTransformations(); const phrasingContentSchema = getPhrasingContentSchema(); const blockContentSchema = getBlockContentSchema( rawTransformations ); - return compact( flatMap( pieces, ( piece ) => { + return Promise.resolve( compact( flatMap( pieces, ( piece ) => { // Already a block from shortcode. if ( typeof piece !== 'string' ) { return piece; @@ -206,5 +206,5 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', ) ); } ); - } ) ); + } ) ) ); } diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index 3c12fee973ff4..169af4ad80bb3 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -554,7 +554,7 @@ describe( 'block parser', () => { // encapsulate the test cases so we can run them multiple time but with a different parse() function function testCases( parse ) { - it( 'should parse the post content, including block attributes', () => { + it( 'should parse the post content, including block attributes', async () => { registerBlockType( 'core/test-block', { attributes: { content: { @@ -570,7 +570,7 @@ describe( 'block parser', () => { title: 'test block', } ); - const parsed = parse( + const parsed = await parse( `` + 'Brisket' + '' @@ -587,13 +587,13 @@ describe( 'block parser', () => { expect( typeof parsed[ 0 ].clientId ).toBe( 'string' ); } ); - it( 'should parse empty post content', () => { - const parsed = parse( '' ); + it( 'should parse empty post content', async () => { + const parsed = await parse( '' ); expect( parsed ).toEqual( [] ); } ); - it( 'should parse the post content, ignoring unknown blocks', () => { + it( 'should parse the post content, ignoring unknown blocks', async () => { registerBlockType( 'core/test-block', { attributes: { content: { @@ -606,7 +606,7 @@ describe( 'block parser', () => { title: 'test block', } ); - const parsed = parse( + const parsed = await parse( '\nRibs\n' + '

Broccoli

' + 'Ribs' @@ -620,10 +620,10 @@ describe( 'block parser', () => { expect( typeof parsed[ 0 ].clientId ).toBe( 'string' ); } ); - it( 'should add the core namespace to un-namespaced blocks', () => { + it( 'should add the core namespace to un-namespaced blocks', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); - const parsed = parse( + const parsed = await parse( '\nBananas\n' ); @@ -631,12 +631,12 @@ describe( 'block parser', () => { expect( parsed[ 0 ].name ).toBe( 'core/test-block' ); } ); - it( 'should ignore blocks with a bad namespace', () => { + it( 'should ignore blocks with a bad namespace', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); setUnknownTypeHandlerName( 'core/unknown-block' ); - const parsed = parse( + const parsed = await parse( '\nBananas\n' + '

Broccoli

' + 'Ribs' @@ -645,13 +645,13 @@ describe( 'block parser', () => { expect( parsed[ 0 ].name ).toBe( 'core/test-block' ); } ); - it( 'should parse the post content, using unknown block handler', () => { + it( 'should parse the post content, using unknown block handler', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', unknownBlockSettings ); setUnknownTypeHandlerName( 'core/unknown-block' ); - const parsed = parse( + const parsed = await parse( '\nBananas\n' + '

Broccoli

' + 'Ribs' @@ -665,13 +665,13 @@ describe( 'block parser', () => { ] ); } ); - it( 'should parse the post content, including raw HTML at each end', () => { + it( 'should parse the post content, including raw HTML at each end', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', unknownBlockSettings ); setUnknownTypeHandlerName( 'core/unknown-block' ); - const parsed = parse( + const parsed = await parse( '

Cauliflower

' + '\nBananas\n' + '\n

Broccoli

\n' + @@ -692,9 +692,9 @@ describe( 'block parser', () => { expect( parsed[ 4 ].attributes.content ).toEqual( '

Romanesco

' ); } ); - it( 'should parse blocks with empty content', () => { + it( 'should parse blocks with empty content', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); - const parsed = parse( + const parsed = await parse( '' ); @@ -704,10 +704,10 @@ describe( 'block parser', () => { ] ); } ); - it( 'should parse void blocks', () => { + it( 'should parse void blocks', async () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/void-block', defaultBlockSettings ); - const parsed = parse( + const parsed = await parse( '' + '' ); @@ -718,7 +718,7 @@ describe( 'block parser', () => { ] ); } ); - it( 'should parse with unicode escaped returned to original representation', () => { + it( 'should parse with unicode escaped returned to original representation', async () => { registerBlockType( 'core/code', { category: 'common', title: 'Code Block', @@ -733,7 +733,7 @@ describe( 'block parser', () => { const content = '$foo = "My \"escaped\" text.";'; const block = createBlock( 'core/code', { content } ); const serialized = serialize( block ); - const parsed = parse( serialized ); + const parsed = await parse( serialized ); expect( parsed[ 0 ].attributes.content ).toBe( content ); } ); } diff --git a/packages/editor/src/components/block-drop-zone/index.js b/packages/editor/src/components/block-drop-zone/index.js index cf6b130343f13..01c21cc89a2c5 100644 --- a/packages/editor/src/components/block-drop-zone/index.js +++ b/packages/editor/src/components/block-drop-zone/index.js @@ -47,8 +47,8 @@ class BlockDropZone extends Component { } } - onHTMLDrop( HTML, position ) { - const blocks = rawHandler( { HTML, mode: 'BLOCKS' } ); + async onHTMLDrop( HTML, position ) { + const blocks = await rawHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { this.props.insertBlocks( blocks, this.getInsertIndex( position ) ); diff --git a/packages/editor/src/components/block-list/block-invalid-warning.js b/packages/editor/src/components/block-list/block-invalid-warning.js index 58583dc3175ac..96b5573957ea5 100644 --- a/packages/editor/src/components/block-list/block-invalid-warning.js +++ b/packages/editor/src/components/block-list/block-invalid-warning.js @@ -44,8 +44,8 @@ export default withDispatch( ( dispatch, { block } ) => { content: block.originalContent, } ) ); }, - convertToBlocks() { - replaceBlock( block.clientId, rawHandler( { + async convertToBlocks() { + replaceBlock( block.clientId, await rawHandler( { HTML: block.originalContent, mode: 'BLOCKS', } ) ); diff --git a/packages/editor/src/components/block-settings-menu/block-html-convert-button.js b/packages/editor/src/components/block-settings-menu/block-html-convert-button.js index 6403e28bff12a..4b69964d473ab 100644 --- a/packages/editor/src/components/block-settings-menu/block-html-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/block-html-convert-button.js @@ -21,9 +21,9 @@ export default compose( }; } ), withDispatch( ( dispatch, { block, canUserUseUnfilteredHTML } ) => ( { - onClick: () => dispatch( 'core/editor' ).replaceBlocks( + onClick: async () => dispatch( 'core/editor' ).replaceBlocks( block.clientId, - rawHandler( { + await rawHandler( { HTML: getBlockContent( block ), mode: 'BLOCKS', canUserUseUnfilteredHTML, diff --git a/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js b/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js index f87149710455f..a07e82b9c5850 100644 --- a/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js @@ -21,9 +21,9 @@ export default compose( }; } ), withDispatch( ( dispatch, { block, canUserUseUnfilteredHTML } ) => ( { - onClick: () => dispatch( 'core/editor' ).replaceBlocks( + onClick: async () => dispatch( 'core/editor' ).replaceBlocks( block.clientId, - rawHandler( { + await rawHandler( { HTML: serialize( block ), mode: 'BLOCKS', canUserUseUnfilteredHTML, diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index 5a4121998a60c..1e302f4f8d5b2 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -111,8 +111,8 @@ export default compose( [ onChange( content ) { editPost( { content } ); }, - onPersist( content ) { - resetBlocks( parse( content ) ); + async onPersist( content ) { + resetBlocks( await parse( content ) ); checkTemplateValidity(); }, }; diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 55eec3c5e2b47..de6198c90cba1 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -251,7 +251,7 @@ export class RichText extends Component { * * @param {PasteEvent} event The paste event as triggered by TinyMCE. */ - onPaste( event ) { + async onPaste( event ) { const clipboardData = event.clipboardData; const { items = [], files = [] } = clipboardData; const item = find( [ ...items, ...files ], ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); @@ -285,11 +285,12 @@ export class RichText extends Component { // Note: a pasted file may have the URL as plain text. if ( item && ! html ) { const file = item.getAsFile ? item.getAsFile() : item; - const content = rawHandler( { + const content = await rawHandler( { HTML: ``, mode: 'BLOCKS', tagName: this.props.tagName, } ); + const shouldReplace = this.props.onReplace && this.isEmpty(); // Allows us to ask for this information when we get a report. @@ -335,7 +336,7 @@ export class RichText extends Component { mode = 'AUTO'; } - const content = rawHandler( { + const content = await rawHandler( { HTML: html, plainText, mode, diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 6b61df24bb734..056e3b1890e23 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -117,30 +117,33 @@ export default { ] ) ); }, - SETUP_EDITOR( action, { getState } ) { + SETUP_EDITOR( action, { dispatch, getState } ) { const { post, autosave } = action; const state = getState(); const template = getTemplate( state ); const templateLock = getTemplateLock( state ); // Parse content as blocks - let blocks; + let waitForBlocks; let isValidTemplate = true; if ( post.content.raw ) { - blocks = parse( post.content.raw ); + waitForBlocks = parse( post.content.raw ); - // Unlocked templates are considered always valid because they act as default values only. - isValidTemplate = ( - ! template || - templateLock !== 'all' || - doBlocksMatchTemplate( blocks, template ) - ); + waitForBlocks.then( ( blocks ) => { + // Unlocked templates are considered always valid because they + // act as default values only. + isValidTemplate = ( + ! template || + templateLock !== 'all' || + doBlocksMatchTemplate( blocks, template ) + ); + } ); } else if ( template ) { - blocks = synchronizeBlocksWithTemplate( [], template ); + waitForBlocks = Promise.resolve( synchronizeBlocksWithTemplate( [], template ) ); } else if ( getDefaultBlockForPostFormat( post.format ) ) { - blocks = [ createBlock( getDefaultBlockForPostFormat( post.format ) ) ]; + waitForBlocks = Promise.resolve( [ createBlock( getDefaultBlockForPostFormat( post.format ) ) ] ); } else { - blocks = []; + waitForBlocks = Promise.resolve( [] ); } // Include auto draft title in edits while not flagging post as dirty @@ -166,11 +169,16 @@ export default { ); } - return [ - setTemplateValidity( isValidTemplate ), - setupEditorState( post, blocks, edits ), - ...( autosaveAction ? [ autosaveAction ] : [] ), - ]; + if ( autosaveAction ) { + dispatch( autosaveAction ); + } + + waitForBlocks.then( ( blocks ) => { + // TODO: Do we need to always set template validity, if the default + // is true and in most cases it will already be valid? + dispatch( setTemplateValidity( isValidTemplate ) ); + dispatch( setupEditorState( post, blocks, edits ) ); + } ); }, SYNCHRONIZE_TEMPLATE( action, { getState } ) { const state = getState(); diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 27dd4e86696f7..89effb6f54d4d 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -66,13 +66,13 @@ export const fetchReusableBlocks = async ( action, store ) => { try { const reusableBlockOrBlocks = await result; - dispatch( receiveReusableBlocksAction( map( + dispatch( receiveReusableBlocksAction( await Promise.all( map( castArray( reusableBlockOrBlocks ), - ( reusableBlock ) => ( { + async ( reusableBlock ) => ( { reusableBlock, - parsedBlock: parse( reusableBlock.content )[ 0 ], + parsedBlock: ( await parse( reusableBlock.content ) )[ 0 ], } ) - ) ) ); + ) ) ) ); dispatch( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index 11263cab61f3f..dae15e60d3718 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -425,7 +425,7 @@ describe( 'effects', () => { } ); } ); - it( 'should return post reset action', () => { + it( 'should return post reset action', ( done ) => { const post = { id: 1, title: { @@ -436,6 +436,16 @@ describe( 'effects', () => { }, status: 'draft', }; + const dispatch = jest.fn().mockImplementation( () => { + if ( dispatch.mock.calls.length < 2 ) { + return; + } + + expect( dispatch ).toHaveBeenCalledWith( setTemplateValidity( true ) ); + expect( dispatch ).toHaveBeenCalledWith( setupEditorState( post, [], {} ) ); + + done(); + } ); const getState = () => ( { settings: { template: null, @@ -443,15 +453,10 @@ describe( 'effects', () => { }, } ); - const result = handler( { post, settings: {} }, { getState } ); - - expect( result ).toEqual( [ - setTemplateValidity( true ), - setupEditorState( post, [], {} ), - ] ); + handler( { post, settings: {} }, { dispatch, getState } ); } ); - it( 'should return block reset with non-empty content', () => { + it( 'should return block reset with non-empty content', ( done ) => { registerBlockType( 'core/test-block', defaultBlockSettings ); const post = { id: 1, @@ -463,6 +468,18 @@ describe( 'effects', () => { }, status: 'draft', }; + const dispatch = jest.fn().mockImplementation( () => { + if ( dispatch.mock.calls.length < 2 ) { + return; + } + + const result = dispatch.mock.calls; + expect( result[ 1 ][ 0 ].blocks ).toHaveLength( 1 ); + expect( dispatch ).toHaveBeenCalledWith( setTemplateValidity( true ) ); + expect( dispatch ).toHaveBeenCalledWith( setupEditorState( post, result[ 1 ][ 0 ].blocks, {} ) ); + + done(); + } ); const getState = () => ( { settings: { template: null, @@ -470,16 +487,10 @@ describe( 'effects', () => { }, } ); - const result = handler( { post }, { getState } ); - - expect( result[ 1 ].blocks ).toHaveLength( 1 ); - expect( result ).toEqual( [ - setTemplateValidity( true ), - setupEditorState( post, result[ 1 ].blocks, {} ), - ] ); + handler( { post }, { dispatch, getState } ); } ); - it( 'should return post setup action only if auto-draft', () => { + it( 'should return post setup action only if auto-draft', ( done ) => { const post = { id: 1, title: { @@ -490,6 +501,16 @@ describe( 'effects', () => { }, status: 'auto-draft', }; + const dispatch = jest.fn().mockImplementation( () => { + if ( dispatch.mock.calls.length < 2 ) { + return; + } + + expect( dispatch ).toHaveBeenCalledWith( setTemplateValidity( true ) ); + expect( dispatch ).toHaveBeenCalledWith( setupEditorState( post, [], { title: 'A History of Pork' } ) ); + + done(); + } ); const getState = () => ( { settings: { template: null, @@ -497,12 +518,7 @@ describe( 'effects', () => { }, } ); - const result = handler( { post }, { getState } ); - - expect( result ).toEqual( [ - setTemplateValidity( true ), - setupEditorState( post, [], { title: 'A History of Pork' } ), - ] ); + handler( { post }, { dispatch, getState } ); } ); } ); } ); diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.spec.js index 8693e4e346d31..ce8723e5abfbb 100644 --- a/test/integration/blocks-raw-handling.spec.js +++ b/test/integration/blocks-raw-handling.spec.js @@ -21,8 +21,8 @@ describe( 'Blocks raw handling', () => { registerCoreBlocks(); } ); - it( 'should filter inline content', () => { - const filtered = rawHandler( { + it( 'should filter inline content', async () => { + const filtered = await rawHandler( { HTML: '

test

', mode: 'INLINE', } ); @@ -31,19 +31,19 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); - it( 'should parse Markdown', () => { - const filtered = rawHandler( { + it( 'should parse Markdown', async () => { + const filtered = ( await rawHandler( { HTML: '* one
* two
* three', plainText: '* one\n* two\n* three', mode: 'AUTO', - } ).map( getBlockContent ).join( '' ); + } ) ).map( getBlockContent ).join( '' ); expect( filtered ).toBe( '' ); expect( console ).toHaveLogged(); } ); - it( 'should parse inline Markdown', () => { - const filtered = rawHandler( { + it( 'should parse inline Markdown', async () => { + const filtered = await rawHandler( { HTML: 'Some **bold** text.', plainText: 'Some **bold** text.', mode: 'AUTO', @@ -53,30 +53,30 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); - it( 'should parse HTML in plainText', () => { - const filtered = rawHandler( { + it( 'should parse HTML in plainText', async () => { + const filtered = ( await rawHandler( { HTML: '<p>Some <strong>bold</strong> text.</p>', plainText: '

Some bold text.

', mode: 'AUTO', - } ).map( getBlockContent ).join( '' ); + } ) ).map( getBlockContent ).join( '' ); expect( filtered ).toBe( '

Some bold text.

' ); expect( console ).toHaveLogged(); } ); - it( 'should parse Markdown with HTML', () => { - const filtered = rawHandler( { + it( 'should parse Markdown with HTML', async () => { + const filtered = ( await rawHandler( { HTML: '', plainText: '# Some heading', mode: 'AUTO', - } ).map( getBlockContent ).join( '' ); + } ) ).map( getBlockContent ).join( '' ); expect( filtered ).toBe( '

Some heading

' ); expect( console ).toHaveLogged(); } ); - it( 'should break up forced inline content', () => { - const filtered = rawHandler( { + it( 'should break up forced inline content', async () => { + const filtered = await rawHandler( { HTML: '

test

test

', mode: 'INLINE', } ); @@ -85,8 +85,8 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); - it( 'should normalize decomposed characters', () => { - const filtered = rawHandler( { + it( 'should normalize decomposed characters', async () => { + const filtered = await rawHandler( { HTML: 'schön', mode: 'INLINE', } ); @@ -114,11 +114,11 @@ describe( 'Blocks raw handling', () => { 'markdown', 'wordpress', ].forEach( ( type ) => { - it( type, () => { + it( type, async () => { const HTML = readFile( path.join( __dirname, `fixtures/${ type }-in.html` ) ); const plainText = readFile( path.join( __dirname, `fixtures/${ type }-in.txt` ) ); const output = readFile( path.join( __dirname, `fixtures/${ type }-out.html` ) ); - const converted = rawHandler( { HTML, plainText, canUserUseUnfilteredHTML: true } ); + const converted = await rawHandler( { HTML, plainText, canUserUseUnfilteredHTML: true } ); const serialized = typeof converted === 'string' ? converted : serialize( converted ); expect( serialized ).toBe( output ); diff --git a/test/integration/full-content/full-content.spec.js b/test/integration/full-content/full-content.spec.js index 0aa2740831a9d..567bd310deeae 100644 --- a/test/integration/full-content/full-content.spec.js +++ b/test/integration/full-content/full-content.spec.js @@ -107,7 +107,7 @@ describe( 'full post content fixture', () => { } ); fileBasenames.forEach( ( f ) => { - it( f, () => { + it( f, async () => { const content = readFixtureFile( f + '.html' ); if ( content === null ) { throw new Error( @@ -146,7 +146,7 @@ describe( 'full post content fixture', () => { ) ); } - const blocksActual = parse( content ); + const blocksActual = await parse( content ); // Block validation logs during deprecation migration. Since this // is expected for deprecated blocks, match on filename and allow.