-
+
"
-`;
+`;
\ No newline at end of file
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-editor-keyboard-shortcuts.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-editor-keyboard-shortcuts.test.js.snap
new file mode 100644
index 0000000000000..a8f360f46b420
--- /dev/null
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/block-editor-keyboard-shortcuts.test.js.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`block editor keyboard shortcuts move blocks multiple blocks selected should move the blocks down 1`] = `
+"
+
First paragraph
+
+
+
+
Second paragraph
+
+
+
+
Third paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks multiple blocks selected should move the blocks down 2`] = `
+"
+
Third paragraph
+
+
+
+
First paragraph
+
+
+
+
Second paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks multiple blocks selected should move the blocks up 1`] = `
+"
+
First paragraph
+
+
+
+
Second paragraph
+
+
+
+
Third paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks multiple blocks selected should move the blocks up 2`] = `
+"
+
Second paragraph
+
+
+
+
Third paragraph
+
+
+
+
First paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks single block selected should move the block down 1`] = `
+"
+
First paragraph
+
+
+
+
Second paragraph
+
+
+
+
Third paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks single block selected should move the block down 2`] = `
+"
+
First paragraph
+
+
+
+
Third paragraph
+
+
+
+
Second paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks single block selected should move the block up 1`] = `
+"
+
First paragraph
+
+
+
+
Second paragraph
+
+
+
+
Third paragraph
+"
+`;
+
+exports[`block editor keyboard shortcuts move blocks single block selected should move the block up 2`] = `
+"
+
Third paragraph
+
+
+
+
First paragraph
+
+
+
+
Second paragraph
+"
+`;
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/editor-modes.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/editor-modes.test.js.snap
new file mode 100644
index 0000000000000..8b5044aeff308
--- /dev/null
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/editor-modes.test.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Editing modes (visual/HTML) saves content when using the shortcut in the Code Editor 1`] = `
+"
+
Hi world!
+"
+`;
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap
index 9672627058066..f033b781c3dd2 100644
--- a/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap
@@ -5,3 +5,63 @@ exports[`Embedding content should allow the user to convert unembeddable URLs to
https://twitter.com/wooyaygutenberg123454312
"
`;
+
+exports[`Embedding content should allow the user to try embedding a failed URL again 1`] = `
+"
+
+"
+`;
+
+exports[`Embedding content should render embeds in the correct state 1`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Embedding content should retry embeds that could not be embedded with trailing slashes, without the trailing slashes 1`] = `
+"
+
+"
+`;
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/rtl.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/rtl.test.js.snap
index 19b4e8c305acc..a8b94682a4596 100644
--- a/packages/e2e-tests/specs/editor/various/__snapshots__/rtl.test.js.snap
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/rtl.test.js.snap
@@ -8,11 +8,11 @@ exports[`RTL should arrow navigate 1`] = `
exports[`RTL should arrow navigate between blocks 1`] = `
"
-
٠
١
+
٠
-
٠
١
٢
+
١١٠
٢
"
`;
@@ -24,7 +24,11 @@ exports[`RTL should merge backward 1`] = `
exports[`RTL should merge forward 1`] = `
"
-
٠١
+
٠
+
+
+
+
"
`;
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap
index 01dc6c0b68269..13766cd2526a1 100644
--- a/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap
@@ -116,6 +116,12 @@ exports[`Writing Flow should merge forwards 1`] = `
"
`;
+exports[`Writing Flow should merge paragraphs 1`] = `
+"
+
12
+"
+`;
+
exports[`Writing Flow should navigate around inline boundaries 1`] = `
"
FirstAfter
diff --git a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
index 6e222ff24e5b7..d6d1496da80a7 100644
--- a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js
@@ -34,6 +34,7 @@ describe( 'adding blocks', () => {
it( 'Should insert content using the placeholder and the regular inserter', async () => {
// This ensures the editor is loaded in navigation mode.
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// Set a tall viewport. The typewriter's intrinsic height can be enough
// to scroll the page on a shorter viewport, thus obscuring the presence
@@ -44,12 +45,17 @@ describe( 'adding blocks', () => {
await clickAtBottom(
await page.$( '.interface-interface-skeleton__content' )
);
- expect( await page.$( '[data-type="core/paragraph"]' ) ).not.toBeNull();
+ expect(
+ await page.waitForSelector( '[data-type="core/paragraph"]' )
+ ).not.toBeNull();
await page.keyboard.type( 'Paragraph block' );
// Using the slash command
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '/quote' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Quote')]`
+ );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Quote block' );
@@ -62,6 +68,9 @@ describe( 'adding blocks', () => {
// append the default block. Pressing backspace on the focused block
// will remove it.
await page.keyboard.type( '/image' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Image')]`
+ );
await page.keyboard.press( 'Enter' );
await page.keyboard.press( 'Enter' );
expect( await getEditedPostContent() ).toMatchSnapshot();
@@ -152,7 +161,6 @@ describe( 'adding blocks', () => {
// Tab to the block list
await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Tab' );
// Expect the block list to be the active element.
activeElementClassList = await page.evaluate(
@@ -184,4 +192,74 @@ describe( 'adding blocks', () => {
'block-editor-inserter__toggle'
);
} );
+
+ // Check for regression of https://github.com/WordPress/gutenberg/issues/23263
+ it( 'inserts blocks at root level when using the root appender while selection is in an inner block', async () => {
+ await insertBlock( 'Buttons' );
+ await page.keyboard.type( '1.1' );
+
+ // After inserting the Buttons block the inner button block should be selected.
+ const selectedButtonBlocks = await page.$$(
+ '.wp-block-button.is-selected'
+ );
+ expect( selectedButtonBlocks.length ).toBe( 1 );
+
+ // Specifically click the root container appender.
+ await page.click(
+ '.block-editor-block-list__layout.is-root-container > .block-list-appender .block-editor-inserter__toggle'
+ );
+
+ // Insert a paragraph block.
+ await page.waitForSelector( '.block-editor-inserter__search-input' );
+ await page.keyboard.type( 'Paragraph' );
+ await page.click( '.editor-block-list-item-paragraph' );
+ await page.keyboard.type( '2' );
+
+ // The snapshot should show a buttons block followed by a paragraph.
+ // The buttons block should contain a single button.
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ // Check for regression of https://github.com/WordPress/gutenberg/issues/24262
+ it( 'inserts a block in proper place after having clicked `Browse All` from inline inserter', async () => {
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'First paragraph' );
+ await insertBlock( 'Heading' );
+ await page.keyboard.type( 'Heading' );
+ await page.keyboard.press( 'Enter' );
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'Second paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Third paragraph' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ // Using the between inserter
+ const insertionPoint = await page.$( '[data-type="core/heading"]' );
+ const rect = await insertionPoint.boundingBox();
+ await page.mouse.move( rect.x + rect.width / 2, rect.y - 10, {
+ steps: 10,
+ } );
+ await page.waitForSelector(
+ '.block-editor-block-list__insertion-point .block-editor-inserter__toggle'
+ );
+ await page.click(
+ '.block-editor-block-list__insertion-point .block-editor-inserter__toggle'
+ );
+
+ const browseAll = await page.waitForSelector(
+ 'button.block-editor-inserter__quick-inserter-expand'
+ );
+ await browseAll.click();
+ const inserterMenuInputSelector =
+ '.edit-post-layout__inserter-panel .block-editor-inserter__search-input';
+ const inserterMenuSearchInput = await page.waitForSelector(
+ inserterMenuInputSelector
+ );
+ inserterMenuSearchInput.type( 'cover' );
+ const coverBlock = await page.waitForSelector(
+ '.block-editor-block-types-list .editor-block-list-item-cover'
+ );
+ await coverBlock.click();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/e2e-tests/specs/editor/various/adding-patterns.test.js b/packages/e2e-tests/specs/editor/various/adding-patterns.test.js
index b8fe904b85178..ab6668d8bb087 100644
--- a/packages/e2e-tests/specs/editor/various/adding-patterns.test.js
+++ b/packages/e2e-tests/specs/editor/various/adding-patterns.test.js
@@ -15,7 +15,7 @@ describe( 'adding blocks', () => {
} );
it( 'should insert a block pattern', async () => {
- await insertPattern( 'Two Buttons' );
+ await insertPattern( 'Two buttons' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
diff --git a/packages/e2e-tests/specs/editor/various/autosave.test.js b/packages/e2e-tests/specs/editor/various/autosave.test.js
index 5780302794aac..34822f8c680aa 100644
--- a/packages/e2e-tests/specs/editor/various/autosave.test.js
+++ b/packages/e2e-tests/specs/editor/various/autosave.test.js
@@ -44,7 +44,7 @@ async function clearSessionStorage() {
async function readSessionStorageAutosave( postId ) {
return page.evaluate(
( key ) => window.sessionStorage.getItem( key ),
- `wp-autosave-block-editor-post-${ postId }`
+ `wp-autosave-block-editor-post-${ postId ? postId : 'auto-draft' }`
);
}
@@ -76,9 +76,14 @@ describe( 'autosave', () => {
} );
it( 'should save to sessionStorage', async () => {
+ // Wait for the original timeout to kick in, it will schedule
+ // another run using the updated interval length of AUTOSAVE_INTERVAL_SECONDS
+ await sleep( 15 );
+
await clickBlockAppender();
await page.keyboard.type( 'before save' );
await saveDraftWithKeyboard();
+ await sleep( 1 );
await page.keyboard.type( ' after save' );
// Wait long enough for local autosave to kick in
@@ -88,19 +93,6 @@ describe( 'autosave', () => {
const autosave = await readSessionStorageAutosave( id );
const { content } = JSON.parse( autosave );
expect( content ).toBe( wrapParagraph( 'before save after save' ) );
-
- // Test throttling by scattering typing
- await page.keyboard.type( ' 1' );
- await sleep( AUTOSAVE_INTERVAL_SECONDS - 4 );
- await page.keyboard.type( '2' );
- await sleep( 2 );
- await page.keyboard.type( '3' );
- await sleep( 2 );
-
- const newAutosave = await readSessionStorageAutosave( id );
- expect( JSON.parse( newAutosave ).content ).toBe(
- wrapParagraph( 'before save after save 123' )
- );
} );
it( 'should recover from sessionStorage', async () => {
@@ -117,6 +109,7 @@ describe( 'autosave', () => {
);
// Reload without saving on the server
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
const notice = await page.$eval(
'.components-notice__content',
@@ -192,6 +185,7 @@ describe( 'autosave', () => {
).toBe( 1 );
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
const notice = await page.$eval(
'.components-notice__content',
( element ) => element.innerText
@@ -219,7 +213,7 @@ describe( 'autosave', () => {
);
expect(
await page.evaluate( () => window.sessionStorage.length )
- ).toBe( 1 );
+ ).toBeGreaterThanOrEqual( 1 );
// Trigger remote autosave
await page.evaluate( () =>
@@ -295,14 +289,14 @@ describe( 'autosave', () => {
);
expect(
await page.evaluate( () => window.sessionStorage.length )
- ).toBe( 1 );
+ ).toBeGreaterThanOrEqual( 1 );
// Bring network down and attempt to save
toggleOfflineMode( true );
saveDraftWithKeyboard();
expect(
await page.evaluate( () => window.sessionStorage.length )
- ).toBe( 1 );
+ ).toBeGreaterThanOrEqual( 1 );
} );
it( "shouldn't conflict with server-side autosave", async () => {
@@ -326,9 +320,10 @@ describe( 'autosave', () => {
);
expect(
await page.evaluate( () => window.sessionStorage.length )
- ).toBe( 1 );
+ ).toBeGreaterThanOrEqual( 1 );
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// FIXME: Occasionally, upon reload, there is no server-provided
// autosave value available, despite our having previously explicitly
diff --git a/packages/e2e-tests/specs/editor/various/block-deletion.test.js b/packages/e2e-tests/specs/editor/various/block-deletion.test.js
index 00027d94137cf..b54348fab54fd 100644
--- a/packages/e2e-tests/specs/editor/various/block-deletion.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-deletion.test.js
@@ -66,11 +66,6 @@ describe( 'block deletion -', () => {
it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => {
// The blocks can't be empty to trigger the toolbar
await page.keyboard.type( 'Paragraph to remove' );
-
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
await clickOnBlockSettingsMenuRemoveBlockButton();
expect( await getEditedPostContent() ).toMatchSnapshot();
@@ -109,6 +104,9 @@ describe( 'block deletion -', () => {
it( 'results in three remaining blocks and positions the caret at the end of the third block', async () => {
// Add an image block since it's easier to click the wrapper on non-textual blocks.
await page.keyboard.type( '/image' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Image')]`
+ );
await page.keyboard.press( 'Enter' );
// Click on something that's not a block.
@@ -156,11 +154,6 @@ describe( 'deleting all blocks', () => {
it( 'results in the default block getting selected', async () => {
await clickBlockAppender();
await page.keyboard.type( 'Paragraph' );
-
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
await clickOnBlockSettingsMenuRemoveBlockButton();
// There is a default block:
@@ -196,6 +189,7 @@ describe( 'deleting all blocks', () => {
// Add and remove a block.
await insertBlock( 'Image' );
+ await page.waitForSelector( 'figure[data-type="core/image"]' );
await page.keyboard.press( 'Backspace' );
// Verify there is no selected block.
diff --git a/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
new file mode 100644
index 0000000000000..6dd9938f4a681
--- /dev/null
+++ b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
@@ -0,0 +1,69 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ createNewPost,
+ clickBlockAppender,
+ getEditedPostContent,
+ pressKeyWithModifier,
+} from '@wordpress/e2e-test-utils';
+
+const createTestParagraphBlocks = async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( 'First paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Second paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Third paragraph' );
+};
+
+describe( 'block editor keyboard shortcuts', () => {
+ beforeEach( async () => {
+ await createNewPost();
+ } );
+
+ describe( 'move blocks', () => {
+ const moveUp = async () => pressKeyWithModifier( 'secondary', 't' );
+ const moveDown = async () => pressKeyWithModifier( 'secondary', 'y' );
+ describe( 'single block selected', () => {
+ it( 'should move the block up', async () => {
+ await createTestParagraphBlocks();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ await moveUp();
+ await moveUp();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'should move the block down', async () => {
+ await createTestParagraphBlocks();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ await page.keyboard.press( 'ArrowUp' );
+ await moveDown();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+ } );
+
+ describe( 'multiple blocks selected', () => {
+ it( 'should move the blocks up', async () => {
+ await createTestParagraphBlocks();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ await page.keyboard.down( 'Shift' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.up( 'Shift' );
+ await moveUp();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'should move the blocks down', async () => {
+ await createTestParagraphBlocks();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.down( 'Shift' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.up( 'Shift' );
+ await moveDown();
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+ } );
+ } );
+} );
diff --git a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
index eceecaaf95ae8..1d268e6dc1cc4 100644
--- a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
@@ -30,12 +30,12 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.press( 'Tab' ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( 'First column' );
// Navigate to the columns blocks.
- await page.click( '[aria-label="Block navigation"]' );
+ await page.click( '[aria-label="Outline"]' );
const columnsBlockMenuItem = (
await page.$x(
"//button[contains(@class,'block-editor-block-navigation-block-select-button') and contains(text(), 'Columns')]"
@@ -46,7 +46,7 @@ describe( 'Navigating the block hierarchy', () => {
// Tweak the columns count.
await openDocumentSettingsSidebar();
await page.focus(
- '.block-editor-block-inspector .components-range-control__number[aria-label="Columns"]'
+ '.block-editor-block-inspector [aria-label="Columns"][type="number"]'
);
await page.keyboard.down( 'Shift' );
await page.keyboard.press( 'ArrowLeft' );
@@ -54,7 +54,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.type( '3' );
// Navigate to the last column block.
- await page.click( '[aria-label="Block navigation"]' );
+ await page.click( '[aria-label="Outline"]' );
const lastColumnsBlockMenuItem = (
await page.$x(
"//button[contains(@class,'block-editor-block-navigation-block-select-button') and contains(text(), 'Column')]"
@@ -66,7 +66,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.press( 'Tab' ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( 'Third column' );
@@ -82,7 +82,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.press( 'Tab' ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( 'First column' );
@@ -109,7 +109,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.press( 'Tab' ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( 'Third column' );
@@ -159,8 +159,8 @@ describe( 'Navigating the block hierarchy', () => {
// Unselect the blocks
await page.click( '.editor-post-title' );
- // Try selecting the group block using the block navigation
- await page.click( '[aria-label="Block navigation"]' );
+ // Try selecting the group block using the Outline
+ await page.click( '[aria-label="Outline"]' );
const groupMenuItem = (
await page.$x(
"//button[contains(@class,'block-editor-block-navigation-block-select-button') and contains(text(), 'Group')]"
diff --git a/packages/e2e-tests/specs/editor/various/block-mover.test.js b/packages/e2e-tests/specs/editor/various/block-mover.test.js
index 3b936f212aba0..c3a04f6f3a6c4 100644
--- a/packages/e2e-tests/specs/editor/various/block-mover.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-mover.test.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { createNewPost } from '@wordpress/e2e-test-utils';
+import { createNewPost, showBlockToolbar } from '@wordpress/e2e-test-utils';
describe( 'block mover', () => {
beforeEach( async () => {
@@ -18,9 +18,7 @@ describe( 'block mover', () => {
// Select a block so the block mover is rendered.
await page.focus( '.block-editor-block-list__block' );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
const blockMover = await page.$$( '.block-editor-block-mover' );
// There should be a block mover.
@@ -35,9 +33,7 @@ describe( 'block mover', () => {
// Select a block so the block mover has the possibility of being rendered.
await page.focus( '.block-editor-block-list__block' );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
// Ensure no block mover exists when only one block exists on the page.
const blockMover = await page.$$( '.block-editor-block-mover' );
diff --git a/packages/e2e-tests/specs/editor/various/block-switcher.test.js b/packages/e2e-tests/specs/editor/various/block-switcher.test.js
index c330f8680af55..f4d8e1031880a 100644
--- a/packages/e2e-tests/specs/editor/various/block-switcher.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-switcher.test.js
@@ -24,11 +24,15 @@ describe( 'Block Switcher', () => {
expect( await hasBlockSwitcher() ).toBeTruthy();
// Verify the correct block transforms appear.
- expect( await getAvailableBlockTransforms() ).toEqual( [
- 'Group',
- 'Paragraph',
- 'Quote',
- ] );
+ expect( await getAvailableBlockTransforms() ).toEqual(
+ expect.arrayContaining( [
+ 'Group',
+ 'Paragraph',
+ 'Quote',
+ 'Heading',
+ 'Pullquote',
+ ] )
+ );
} );
it( 'Should show the expected block transforms on the list block when the quote block is removed', async () => {
@@ -46,18 +50,26 @@ describe( 'Block Switcher', () => {
expect( await hasBlockSwitcher() ).toBeTruthy();
// Verify the correct block transforms appear.
- expect( await getAvailableBlockTransforms() ).toEqual( [
- 'Group',
- 'Paragraph',
- ] );
+ expect( await getAvailableBlockTransforms() ).toEqual(
+ expect.arrayContaining( [
+ 'Group',
+ 'Paragraph',
+ 'Pullquote',
+ 'Heading',
+ ] )
+ );
} );
it( 'Should not show the block switcher if all the blocks the list block transforms into are removed', async () => {
// Remove the paragraph and quote block from the list of registered blocks.
await page.evaluate( () => {
- [ 'core/quote', 'core/paragraph', 'core/group' ].map( ( block ) =>
- wp.blocks.unregisterBlockType( block )
- );
+ [
+ 'core/quote',
+ 'core/pullquote',
+ 'core/paragraph',
+ 'core/group',
+ 'core/heading',
+ ].map( ( block ) => wp.blocks.unregisterBlockType( block ) );
} );
// Insert a list block.
diff --git a/packages/e2e-tests/specs/editor/various/dropdown-menu.test.js b/packages/e2e-tests/specs/editor/various/dropdown-menu.test.js
new file mode 100644
index 0000000000000..1ab5730ab6677
--- /dev/null
+++ b/packages/e2e-tests/specs/editor/various/dropdown-menu.test.js
@@ -0,0 +1,143 @@
+/**
+ * WordPress dependencies
+ */
+import { createNewPost, pressKeyTimes } from '@wordpress/e2e-test-utils';
+
+const moreMenuButtonSelector = '.components-button[aria-label="Options"]';
+const moreMenuDropdownSelector =
+ '.components-dropdown-menu__menu[aria-label="Options"]';
+const menuItemsSelector = [ 'menuitem', 'menuitemcheckbox', 'menuitemradio' ]
+ .map( ( role ) => `${ moreMenuDropdownSelector } [role="${ role }"]` )
+ .join( ',' );
+
+describe( 'Dropdown Menu', () => {
+ beforeEach( async () => {
+ await createNewPost();
+ } );
+
+ it( 'allows navigation through each item using arrow keys', async () => {
+ await page.click( moreMenuButtonSelector );
+ const menuItems = await page.$$( menuItemsSelector );
+
+ // Catch any issues with the selector, which could cause a false positive test result.
+ expect( menuItems.length ).toBeGreaterThan( 0 );
+
+ let activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+ const [ firstMenuItem ] = menuItems;
+ const firstMenuItemText = await firstMenuItem.evaluate(
+ ( element ) => element.textContent
+ );
+
+ // Expect the first menu item to be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+
+ // Arrow down to the last item.
+ await pressKeyTimes( 'ArrowDown', menuItems.length - 1 );
+
+ activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+
+ const [ lastMenuItem ] = menuItems.slice( -1 );
+ const lastMenuItemText = await lastMenuItem.evaluate(
+ ( element ) => element.textContent
+ );
+
+ // Expect the last menu item to be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( lastMenuItemText );
+
+ // Arrow back up to the first item.
+ await pressKeyTimes( 'ArrowUp', menuItems.length - 1 );
+
+ activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+
+ // Expect the first menu item to be focused again.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+ } );
+
+ it( 'loops to the beginning and end when navigating past the boundaries of the menu', async () => {
+ await page.click( moreMenuButtonSelector );
+ const menuItems = await page.$$( menuItemsSelector );
+
+ // Catch any issues with the selector, which could cause a false positive test result.
+ expect( menuItems.length ).toBeGreaterThan( 0 );
+
+ let activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+ const [ firstMenuItem ] = menuItems;
+ const firstMenuItemText = await firstMenuItem.evaluate(
+ ( element ) => element.textContent
+ );
+
+ // Expect the first menu item to be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+
+ // Arrow up to the last item.
+ await page.keyboard.press( 'ArrowUp' );
+
+ activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+
+ const [ lastMenuItem ] = menuItems.slice( -1 );
+ const lastMenuItemText = await lastMenuItem.evaluate(
+ ( element ) => element.textContent
+ );
+
+ // Expect the last menu item to be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( lastMenuItemText );
+
+ // Arrow back down to the first item.
+ await page.keyboard.press( 'ArrowDown' );
+
+ activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+
+ // Expect the first menu item to be focused again.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+ } );
+
+ it( 'ignores arrow key navigation that is orthogonal to the orientation of the menu, but stays open', async () => {
+ await page.click( moreMenuButtonSelector );
+ const menuItems = await page.$$( menuItemsSelector );
+
+ // Catch any issues with the selector, which could cause a false positive test result.
+ expect( menuItems.length ).toBeGreaterThan( 0 );
+
+ let activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+ const [ firstMenuItem ] = menuItems;
+ const firstMenuItemText = await firstMenuItem.evaluate(
+ ( element ) => element.textContent
+ );
+
+ // Expect the first menu item to be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+
+ // Press left and right keys an arbitrary (but > 1) number of times.
+ await pressKeyTimes( 'ArrowLeft', 5 );
+ await pressKeyTimes( 'ArrowRight', 5 );
+
+ activeElementText = await page.evaluate(
+ () => document.activeElement.textContent
+ );
+
+ // Expect the first menu item to still be focused.
+ expect( activeElementText ).toBeDefined();
+ expect( activeElementText ).toBe( firstMenuItemText );
+ } );
+} );
diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js
index 6aef17cd69594..53c280301316a 100644
--- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js
+++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js
@@ -5,7 +5,10 @@ import {
clickBlockAppender,
clickBlockToolbarButton,
createNewPost,
+ getCurrentPostContent,
switchEditorModeTo,
+ pressKeyTimes,
+ pressKeyWithModifier,
} from '@wordpress/e2e-test-utils';
describe( 'Editing modes (visual/HTML)', () => {
@@ -22,10 +25,6 @@ describe( 'Editing modes (visual/HTML)', () => {
);
expect( visualBlock ).toHaveLength( 1 );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
// Change editing mode from "Visual" to "HTML".
await clickBlockToolbarButton( 'More options' );
let changeModeButton = await page.waitForXPath(
@@ -39,10 +38,6 @@ describe( 'Editing modes (visual/HTML)', () => {
);
expect( htmlBlock ).toHaveLength( 1 );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
// Change editing mode from "HTML" back to "Visual".
await clickBlockToolbarButton( 'More options' );
changeModeButton = await page.waitForXPath(
@@ -58,10 +53,6 @@ describe( 'Editing modes (visual/HTML)', () => {
} );
it( 'should display sidebar in HTML mode', async () => {
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
// Change editing mode from "Visual" to "HTML".
await clickBlockToolbarButton( 'More options' );
const changeModeButton = await page.waitForXPath(
@@ -78,10 +69,6 @@ describe( 'Editing modes (visual/HTML)', () => {
} );
it( 'should update HTML in HTML mode when sidebar is used', async () => {
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
// Change editing mode from "Visual" to "HTML".
await clickBlockToolbarButton( 'More options' );
const changeModeButton = await page.waitForXPath(
@@ -150,4 +137,29 @@ describe( 'Editing modes (visual/HTML)', () => {
);
expect( disabledInserter ).not.toBeNull();
} );
+
+ // Test for regressions of https://github.com/WordPress/gutenberg/issues/24054.
+ it( 'saves content when using the shortcut in the Code Editor', async () => {
+ await switchEditorModeTo( 'Code' );
+
+ const textContent = await page.evaluate(
+ () => document.querySelector( '.editor-post-text-editor' ).value
+ );
+ const editPosition = textContent.indexOf( 'Hello' );
+
+ // Replace the word 'Hello' with 'Hi'.
+ await page.click( '.editor-post-title__input' );
+ await page.keyboard.press( 'Tab' );
+ await pressKeyTimes( 'ArrowRight', editPosition );
+ await pressKeyTimes( 'Delete', 5 );
+ await page.keyboard.type( 'Hi' );
+
+ // Save the post using the shortcut.
+ await pressKeyWithModifier( 'primary', 's' );
+ await page.waitForSelector( '.editor-post-saved-state.is-saved' );
+
+ await switchEditorModeTo( 'Visual' );
+
+ expect( await getCurrentPostContent() ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/e2e-tests/specs/editor/various/embedding.test.js b/packages/e2e-tests/specs/editor/various/embedding.test.js
index 9e30ec116f3c3..170965215eceb 100644
--- a/packages/e2e-tests/specs/editor/various/embedding.test.js
+++ b/packages/e2e-tests/specs/editor/various/embedding.test.js
@@ -151,6 +151,17 @@ const MOCK_RESPONSES = [
},
];
+async function insertEmbed( URL ) {
+ await clickBlockAppender();
+ await page.keyboard.type( '/embed' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Embed')]`
+ );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( URL );
+ await page.keyboard.press( 'Enter' );
+}
+
describe( 'Embedding content', () => {
beforeEach( async () => {
await setUpResponseMocking( MOCK_RESPONSES );
@@ -159,87 +170,50 @@ describe( 'Embedding content', () => {
it( 'should render embeds in the correct state', async () => {
// Valid embed. Should render valid figure element.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'https://twitter.com/notnownikki' );
- await page.keyboard.press( 'Enter' );
- await page.waitForSelector( 'figure.wp-block-embed-twitter' );
+ await insertEmbed( 'https://twitter.com/notnownikki' );
+ await page.waitForSelector( 'figure.wp-block-embed' );
// Valid provider; invalid content. Should render failed, edit state.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type(
- 'https://twitter.com/wooyaygutenberg123454312'
- );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://twitter.com/wooyaygutenberg123454312' );
await page.waitForSelector(
'input[value="https://twitter.com/wooyaygutenberg123454312"]'
);
// WordPress invalid content. Should render failed, edit state.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'https://wordpress.org/gutenberg/handbook/' );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://wordpress.org/gutenberg/handbook/' );
await page.waitForSelector(
'input[value="https://wordpress.org/gutenberg/handbook/"]'
);
// Provider whose oembed API has gone wrong. Should render failed, edit
// state.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'https://twitter.com/thatbunty' );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://twitter.com/thatbunty' );
await page.waitForSelector(
'input[value="https://twitter.com/thatbunty"]'
);
// WordPress content that can be embedded. Should render valid figure
// element.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type(
+ await insertEmbed(
'https://wordpress.org/gutenberg/handbook/block-api/attributes/'
);
- await page.keyboard.press( 'Enter' );
- await page.waitForSelector( 'figure.wp-block-embed-wordpress' );
+ await page.waitForSelector( 'figure.wp-block-embed' );
// Video content. Should render valid figure element, and include the
// aspect ratio class.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type(
- 'https://www.youtube.com/watch?v=lXMskKTw3Bc'
- );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' );
await page.waitForSelector(
- 'figure.wp-block-embed-youtube.wp-embed-aspect-16-9'
+ 'figure.wp-block-embed.is-type-video.wp-embed-aspect-16-9'
);
// Photo content. Should render valid figure element.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'https://cloudup.com/cQFlxqtY4ob' );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://cloudup.com/cQFlxqtY4ob' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
} );
it( 'should allow the user to convert unembeddable URLs to a paragraph with a link in it', async () => {
// URL that can't be embedded.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type(
- 'https://twitter.com/wooyaygutenberg123454312'
- );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://twitter.com/wooyaygutenberg123454312' );
// Wait for the request to fail and present an error. Since placeholder
// has styles applied which depend on resize observer, wait for the
@@ -254,25 +228,15 @@ describe( 'Embedding content', () => {
} );
it( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async () => {
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- // This URL can't be embedded, but without the trailing slash, it can.
- await page.keyboard.type( 'https://twitter.com/notnownikki/' );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://twitter.com/notnownikki/' );
// The twitter block should appear correctly.
- await page.waitForSelector( 'figure.wp-block-embed-twitter' );
+ await page.waitForSelector( 'figure.wp-block-embed' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
} );
it( 'should allow the user to try embedding a failed URL again', async () => {
// URL that can't be embedded.
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type(
- 'https://twitter.com/wooyaygutenberg123454312'
- );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( 'https://twitter.com/wooyaygutenberg123454312' );
// Wait for the request to fail and present an error. Since placeholder
// has styles applied which depend on resize observer, wait for the
@@ -294,7 +258,8 @@ describe( 'Embedding content', () => {
},
] );
await clickButton( 'Try again' );
- await page.waitForSelector( 'figure.wp-block-embed-twitter' );
+ await page.waitForSelector( 'figure.wp-block-embed' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
} );
it( 'should switch to the WordPress block correctly', async () => {
@@ -313,13 +278,9 @@ describe( 'Embedding content', () => {
// Start a new post, embed the previous post.
await createNewPost();
- await clickBlockAppender();
- await page.keyboard.type( '/embed' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( postUrl );
- await page.keyboard.press( 'Enter' );
+ await insertEmbed( postUrl );
// Check the block has become a WordPress block.
- await page.waitForSelector( '.wp-block-embed-wordpress' );
+ await page.waitForSelector( 'figure.wp-block-embed' );
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/invalid-block.test.js b/packages/e2e-tests/specs/editor/various/invalid-block.test.js
index 7185017ddfd3c..7bb46d364e39a 100644
--- a/packages/e2e-tests/specs/editor/various/invalid-block.test.js
+++ b/packages/e2e-tests/specs/editor/various/invalid-block.test.js
@@ -36,8 +36,14 @@ describe( 'invalid blocks', () => {
expect( console ).toHaveErrored();
expect( console ).toHaveWarned();
- // Click on the 'resolve' button
- await page.click( '.block-editor-warning__actions button' );
+ // Click on the 'three-dots' menu toggle
+ await page.click(
+ '.block-editor-warning__actions button[aria-label="More options"]'
+ );
+
+ // Click on the 'Resolve' button
+ const [ resolveButton ] = await page.$x( '//button[text()="Resolve"]' );
+ await resolveButton.click();
// Check we get the resolve modal with the appropriate contents
const htmlBlockContent = await page.$eval(
diff --git a/packages/e2e-tests/specs/editor/various/is-typing.test.js b/packages/e2e-tests/specs/editor/various/is-typing.test.js
index 8161118d65944..6f6c0f99fa73a 100644
--- a/packages/e2e-tests/specs/editor/various/is-typing.test.js
+++ b/packages/e2e-tests/specs/editor/various/is-typing.test.js
@@ -1,7 +1,11 @@
/**
* WordPress dependencies
*/
-import { clickBlockAppender, createNewPost } from '@wordpress/e2e-test-utils';
+import {
+ clickBlockAppender,
+ createNewPost,
+ showBlockToolbar,
+} from '@wordpress/e2e-test-utils';
describe( 'isTyping', () => {
beforeEach( async () => {
@@ -39,7 +43,7 @@ describe( 'isTyping', () => {
it( 'should not close the dropdown when typing in it', async () => {
// Adds a Dropdown with an input to all blocks
await page.evaluate( () => {
- const { Dropdown, Button, Fill } = wp.components;
+ const { Dropdown, ToolbarButton, Fill } = wp.components;
const { createElement: el, Fragment } = wp.element;
function AddDropdown( BlockListBlock ) {
return ( props ) => {
@@ -52,7 +56,7 @@ describe( 'isTyping', () => {
el( Dropdown, {
renderToggle: ( { onToggle } ) =>
el(
- Button,
+ ToolbarButton,
{
onClick: onToggle,
className: 'dropdown-open',
@@ -83,8 +87,7 @@ describe( 'isTyping', () => {
await page.keyboard.type( 'Type' );
// Show Toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
// Open the dropdown
await page.click( '.dropdown-open' );
diff --git a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
index d8f4e06458bf7..99a9be8268fe1 100644
--- a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
@@ -7,6 +7,7 @@ import {
pressKeyWithModifier,
clickBlockAppender,
getEditedPostContent,
+ showBlockToolbar,
} from '@wordpress/e2e-test-utils';
async function getActiveLabel() {
@@ -22,7 +23,6 @@ const navigateToContentEditorTop = async () => {
// Use 'Ctrl+`' to return to the top of the editor
await pressKeyWithModifier( 'ctrl', '`' );
await pressKeyWithModifier( 'ctrl', '`' );
- await pressKeyWithModifier( 'ctrl', '`' );
};
const tabThroughParagraphBlock = async ( paragraphText ) => {
@@ -89,10 +89,7 @@ describe( 'Order of block keyboard navigation', () => {
// Select the middle block.
await page.keyboard.press( 'ArrowUp' );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
-
+ await showBlockToolbar();
await navigateToContentEditorTop();
await tabThroughParagraphBlock( 'Paragraph 1' );
@@ -189,7 +186,7 @@ describe( 'Order of block keyboard navigation', () => {
expect( await getActiveLabel() ).toBe( 'Multiple selected blocks' );
await page.keyboard.press( 'Tab' );
- await expect( await getActiveLabel() ).toBe( 'Document' );
+ await expect( await getActiveLabel() ).toBe( 'Post' );
await pressKeyWithModifier( 'shift', 'Tab' );
await expect( await getActiveLabel() ).toBe(
diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js
index d13f4dc4a9133..21c50094a0d1f 100644
--- a/packages/e2e-tests/specs/editor/various/links.test.js
+++ b/packages/e2e-tests/specs/editor/various/links.test.js
@@ -7,6 +7,7 @@ import {
getEditedPostContent,
createNewPost,
pressKeyWithModifier,
+ showBlockToolbar,
} from '@wordpress/e2e-test-utils';
describe( 'Links', () => {
@@ -300,9 +301,7 @@ describe( 'Links', () => {
// Make a collapsed selection inside the link
await page.keyboard.press( 'ArrowLeft' );
await page.keyboard.press( 'ArrowRight' );
- // Move the mouse to show the block toolbar
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
const [ editButton ] = await page.$x( '//button[text()="Edit"]' );
await editButton.click();
await waitForAutoFocus();
diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
index e4537418ea693..5623878d744c6 100644
--- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
+++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
@@ -306,6 +306,9 @@ describe( 'Multi-block selection', () => {
await page.keyboard.type( '1' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '/cover' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Cover')]`
+ );
await page.keyboard.press( 'Enter' );
await page.click( '.components-circular-option-picker__option' );
await page.keyboard.type( '2' );
diff --git a/packages/e2e-tests/specs/editor/various/new-post.test.js b/packages/e2e-tests/specs/editor/various/new-post.test.js
index 41db6ce7148b2..e8baff2d573fb 100644
--- a/packages/e2e-tests/specs/editor/various/new-post.test.js
+++ b/packages/e2e-tests/specs/editor/various/new-post.test.js
@@ -70,6 +70,7 @@ describe( 'new editor state', () => {
await page.waitForSelector( '.editor-post-saved-state.is-saved' );
// Reload the browser so a post is loaded with a title.
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
const activeElementClasses = await page.evaluate( () => {
return Object.values( document.activeElement.classList );
diff --git a/packages/e2e-tests/specs/editor/various/nux.test.js b/packages/e2e-tests/specs/editor/various/nux.test.js
index 76f0f1a435966..bb4ca71791030 100644
--- a/packages/e2e-tests/specs/editor/various/nux.test.js
+++ b/packages/e2e-tests/specs/editor/various/nux.test.js
@@ -87,6 +87,7 @@ describe( 'New User Experience (NUX)', () => {
// Reload the editor
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// Guide should be closed
welcomeGuide = await page.$( '.edit-post-welcome-guide' );
@@ -108,6 +109,7 @@ describe( 'New User Experience (NUX)', () => {
// Reload the editor
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// Guide should be closed
welcomeGuide = await page.$( '.edit-post-welcome-guide' );
diff --git a/packages/e2e-tests/specs/editor/various/preferences.test.js b/packages/e2e-tests/specs/editor/various/preferences.test.js
index b21c850b41651..2640d382e74b7 100644
--- a/packages/e2e-tests/specs/editor/various/preferences.test.js
+++ b/packages/e2e-tests/specs/editor/various/preferences.test.js
@@ -30,7 +30,7 @@ describe( 'preferences', () => {
it( 'remembers sidebar dismissal between sessions', async () => {
// Open by default.
- expect( await getActiveSidebarTabText() ).toBe( 'Document' );
+ expect( await getActiveSidebarTabText() ).toBe( 'Post' );
// Change to "Block" tab.
await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' );
@@ -41,7 +41,8 @@ describe( 'preferences', () => {
// See: https://github.com/WordPress/gutenberg/issues/6377
// See: https://github.com/WordPress/gutenberg/pull/8995
await page.reload();
- expect( await getActiveSidebarTabText() ).toBe( 'Document' );
+ await page.waitForSelector( '.edit-post-layout' );
+ expect( await getActiveSidebarTabText() ).toBe( 'Post' );
// Dismiss
await page.click(
@@ -51,6 +52,7 @@ describe( 'preferences', () => {
// Remember after reload.
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
expect( await getActiveSidebarTabText() ).toBe( null );
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
index f57b1f96c3a73..05a04312dfa3e 100644
--- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
@@ -3,11 +3,13 @@
*/
import {
insertBlock,
+ insertReusableBlock,
createNewPost,
clickBlockToolbarButton,
pressKeyWithModifier,
- searchForBlock,
+ searchForReusableBlock,
getEditedPostContent,
+ trashAllPosts,
} from '@wordpress/e2e-test-utils';
function waitForAndAcceptDialog() {
@@ -21,6 +23,10 @@ describe( 'Reusable blocks', () => {
await createNewPost();
} );
+ afterAll( async () => {
+ await trashAllPosts( 'wp_block' );
+ } );
+
beforeEach( async () => {
// Remove all blocks from the post so that we're working with a clean slate
await page.evaluate( () => {
@@ -114,7 +120,7 @@ describe( 'Reusable blocks', () => {
it( 'can be inserted and edited', async () => {
// Insert the reusable block we created above
- await insertBlock( 'Greeting block' );
+ await insertReusableBlock( 'Greeting block' );
// Put the reusable block in edit mode
const editButton = await page.waitForXPath(
@@ -198,7 +204,7 @@ describe( 'Reusable blocks', () => {
// Step 3. Insert the block created in Step 1.
- await insertBlock( 'Awesome block' );
+ await insertReusableBlock( 'Awesome block' );
// Check that we have a reusable block on the page
const block = await page.$(
@@ -216,14 +222,10 @@ describe( 'Reusable blocks', () => {
it( 'can be converted to a regular block', async () => {
// Insert the reusable block we edited above
- await insertBlock( 'Surprised greeting block' );
+ await insertReusableBlock( 'Surprised greeting block' );
// Convert block to a regular block
- await clickBlockToolbarButton( 'More options' );
- const convertButton = await page.waitForXPath(
- '//button[text()="Convert to Regular Block"]'
- );
- await convertButton.click();
+ await clickBlockToolbarButton( 'Convert to regular blocks', 'content' );
// Check that we have a paragraph block on the page
const block = await page.$(
@@ -241,7 +243,7 @@ describe( 'Reusable blocks', () => {
it( 'can be deleted', async () => {
// Insert the reusable block we edited above
- await insertBlock( 'Surprised greeting block' );
+ await insertReusableBlock( 'Surprised greeting block' );
// Delete the block and accept the confirmation dialog
await clickBlockToolbarButton( 'More options' );
@@ -259,7 +261,7 @@ describe( 'Reusable blocks', () => {
expect( await getEditedPostContent() ).toBe( '' );
// Search for the block in the inserter
- await searchForBlock( 'Surprised greeting block' );
+ await searchForReusableBlock( 'Surprised greeting block' );
// Check that we couldn't find it
const items = await page.$$(
@@ -322,14 +324,10 @@ describe( 'Reusable blocks', () => {
it( 'multi-selection reusable block can be converted back to regular blocks', async () => {
// Insert the reusable block we edited above
- await insertBlock( 'Multi-selection reusable block' );
+ await insertReusableBlock( 'Multi-selection reusable block' );
// Convert block to a regular block
- await clickBlockToolbarButton( 'More options' );
- const convertButton = await page.waitForXPath(
- '//button[text()="Convert to Regular Block"]'
- );
- await convertButton.click();
+ await clickBlockToolbarButton( 'Convert to regular blocks', 'content' );
// Check that we have two paragraph blocks on the page
expect( await getEditedPostContent() ).toMatchSnapshot();
diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js
index 1ac2080a1631c..17e064b7c5852 100644
--- a/packages/e2e-tests/specs/editor/various/rich-text.test.js
+++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js
@@ -7,6 +7,7 @@ import {
insertBlock,
clickBlockAppender,
pressKeyWithModifier,
+ showBlockToolbar,
} from '@wordpress/e2e-test-utils';
describe( 'RichText', () => {
@@ -84,12 +85,10 @@ describe( 'RichText', () => {
it( 'should return focus when pressing formatting button', async () => {
await clickBlockAppender();
await page.keyboard.type( 'Some ' );
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
await page.click( '[aria-label="Bold"]' );
await page.keyboard.type( 'bold' );
- await page.mouse.move( 0, 0 );
- await page.mouse.move( 10, 10 );
+ await showBlockToolbar();
await page.click( '[aria-label="Bold"]' );
await page.keyboard.type( '.' );
diff --git a/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js b/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js
index 84992f997efab..35b51001788cd 100644
--- a/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js
+++ b/packages/e2e-tests/specs/editor/various/sidebar-permalink-panel.test.js
@@ -9,6 +9,8 @@ import {
publishPost,
} from '@wordpress/e2e-test-utils';
+const permalinkPanelXPath = `//div[contains(@class, "edit-post-sidebar")]//button[contains(@class, "components-panel__body-toggle") and contains(text(),"Permalink")]`;
+
// This tests are not together with the remaining sidebar tests,
// because we need to publish/save a post, to correctly test the permalink panel.
// The sidebar test suit enforces that focus is never lost, but during save operations
@@ -28,9 +30,7 @@ describe( 'Sidebar Permalink Panel', () => {
const { removeEditorPanel } = wp.data.dispatch( 'core/edit-post' );
removeEditorPanel( 'post-link' );
} );
- expect(
- await findSidebarPanelWithTitle( 'Permalink' )
- ).toBeUndefined();
+ expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] );
} );
it( 'should not render link panel when post is publicly queryable but not public', async () => {
@@ -39,9 +39,7 @@ describe( 'Sidebar Permalink Panel', () => {
await publishPost();
// Start editing again.
await page.type( '.editor-post-title__input', ' (Updated)' );
- expect(
- await findSidebarPanelWithTitle( 'Permalink' )
- ).toBeUndefined();
+ expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] );
} );
it( 'should not render link panel when post is public but not publicly queryable', async () => {
@@ -50,9 +48,7 @@ describe( 'Sidebar Permalink Panel', () => {
await publishPost();
// Start editing again.
await page.type( '.editor-post-title__input', ' (Updated)' );
- expect(
- await findSidebarPanelWithTitle( 'Permalink' )
- ).toBeUndefined();
+ expect( await page.$x( permalinkPanelXPath ) ).toEqual( [] );
} );
it( 'should render link panel when post is public and publicly queryable', async () => {
diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js
index a9d4dfdd2135e..2be81eac943e7 100644
--- a/packages/e2e-tests/specs/editor/various/sidebar.test.js
+++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js
@@ -14,7 +14,7 @@ import {
const SIDEBAR_SELECTOR = '.edit-post-sidebar';
const ACTIVE_SIDEBAR_TAB_SELECTOR = '.edit-post-sidebar__panel-tab.is-active';
-const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Document';
+const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Post';
describe( 'Sidebar', () => {
afterEach( () => {
@@ -99,11 +99,11 @@ describe( 'Sidebar', () => {
await pressKeyWithModifier( 'ctrl', '`' );
await pressKeyWithModifier( 'ctrl', '`' );
- // Tab lands at first (presumed selected) option "Document".
+ // Tab lands at first (presumed selected) option "Post".
await page.keyboard.press( 'Tab' );
const isActiveDocumentTab = await page.evaluate(
() =>
- document.activeElement.textContent === 'Document' &&
+ document.activeElement.textContent === 'Post' &&
document.activeElement.classList.contains( 'is-active' )
);
expect( isActiveDocumentTab ).toBe( true );
@@ -122,7 +122,6 @@ describe( 'Sidebar', () => {
it( 'should be possible to programmatically remove Document Settings panels', async () => {
await createNewPost();
await enableFocusLossObservation();
-
await openDocumentSettingsSidebar();
expect( await findSidebarPanelWithTitle( 'Categories' ) ).toBeDefined();
@@ -147,19 +146,27 @@ describe( 'Sidebar', () => {
removeEditorPanel( 'post-status' );
} );
+ const getPanelToggleSelector = ( panelTitle ) => {
+ return `//div[contains(@class, "edit-post-sidebar")]//button[contains(@class, "components-panel__body-toggle") and contains(text(),"${ panelTitle }")]`;
+ };
+
expect(
- await findSidebarPanelWithTitle( 'Categories' )
- ).toBeUndefined();
- expect( await findSidebarPanelWithTitle( 'Tags' ) ).toBeUndefined();
+ await page.$x( getPanelToggleSelector( 'Categories' ) )
+ ).toEqual( [] );
+ expect( await page.$x( getPanelToggleSelector( 'Tags' ) ) ).toEqual(
+ []
+ );
expect(
- await findSidebarPanelWithTitle( 'Featured image' )
- ).toBeUndefined();
- expect( await findSidebarPanelWithTitle( 'Excerpt' ) ).toBeUndefined();
+ await page.$x( getPanelToggleSelector( 'Featured image' ) )
+ ).toEqual( [] );
+ expect( await page.$x( getPanelToggleSelector( 'Excerpt' ) ) ).toEqual(
+ []
+ );
expect(
- await findSidebarPanelWithTitle( 'Discussion' )
- ).toBeUndefined();
+ await page.$x( getPanelToggleSelector( 'Discussion' ) )
+ ).toEqual( [] );
expect(
- await findSidebarPanelWithTitle( 'Status & visibility' )
- ).toBeUndefined();
+ await page.$x( getPanelToggleSelector( 'Status & visibility' ) )
+ ).toEqual( [] );
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/taxonomies.test.js b/packages/e2e-tests/specs/editor/various/taxonomies.test.js
index 45ac6cbc1e320..2ed1c18d2c361 100644
--- a/packages/e2e-tests/specs/editor/various/taxonomies.test.js
+++ b/packages/e2e-tests/specs/editor/various/taxonomies.test.js
@@ -128,6 +128,62 @@ describe( 'Taxonomies', () => {
expect( selectedCategories[ 0 ] ).toEqual( 'z rand category 1' );
} );
+ it( "should be able to create a new tag with ' on the name", async () => {
+ await createNewPost();
+
+ await openDocumentSettingsSidebar();
+
+ await openSidebarPanelWithTitle( 'Tags' );
+
+ // If the user has no permission to add a new tag finish the test.
+ if ( ! ( await canCreatTermInTaxonomy( 'tags' ) ) ) {
+ return;
+ }
+
+ const tagsPanel = await findSidebarPanelWithTitle( 'Tags' );
+ const tagInput = await tagsPanel.$(
+ '.components-form-token-field__input'
+ );
+
+ // Click the tag input field.
+ await tagInput.click();
+
+ const tagName = "tag'-" + random( 1, Number.MAX_SAFE_INTEGER );
+
+ // Type the category name in the field.
+ await tagInput.type( tagName );
+
+ // Press enter to create a new tag.
+ await tagInput.press( 'Enter' );
+
+ await page.waitForSelector( TAG_TOKEN_SELECTOR );
+
+ // Get an array with the tags of the post.
+ let tags = await getCurrentTags();
+
+ // The post should only contain the tag we added.
+ expect( tags ).toHaveLength( 1 );
+ expect( tags[ 0 ] ).toEqual( tagName );
+
+ // Type something in the title so we can publish the post.
+ await page.type( '.editor-post-title__input', 'Hello World' );
+
+ // Publish the post.
+ await publishPost();
+
+ // Reload the editor.
+ await page.reload();
+
+ // Wait for the tags to load.
+ await page.waitForSelector( '.components-form-token-field__token' );
+
+ tags = await getCurrentTags();
+
+ // The tag selection was persisted after the publish process.
+ expect( tags ).toHaveLength( 1 );
+ expect( tags[ 0 ] ).toEqual( tagName );
+ } );
+
it( 'should be able to open the tags panel and create a new tag if the user has the right capabilities', async () => {
await createNewPost();
diff --git a/packages/e2e-tests/specs/editor/various/typewriter.test.js b/packages/e2e-tests/specs/editor/various/typewriter.test.js
index 2661d69acd905..a4ed30951220b 100644
--- a/packages/e2e-tests/specs/editor/various/typewriter.test.js
+++ b/packages/e2e-tests/specs/editor/various/typewriter.test.js
@@ -21,6 +21,9 @@ describe( 'TypeWriter', () => {
// Create first block.
await page.keyboard.press( 'Enter' );
+ // Create second block.
+ await page.keyboard.press( 'Enter' );
+
const initialPosition = await getCaretPosition();
// The page shouldn't be scrolled when it's being filled.
@@ -118,8 +121,11 @@ describe( 'TypeWriter', () => {
await page.keyboard.press( 'Enter' );
// Create second block.
await page.keyboard.press( 'Enter' );
+ // Create third block.
+ await page.keyboard.press( 'Enter' );
// Move to first block.
await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'ArrowUp' );
const initialPosition = await getCaretPosition();
diff --git a/packages/e2e-tests/specs/editor/various/undo.test.js b/packages/e2e-tests/specs/editor/various/undo.test.js
index ac9864b981493..30b3a4c80d494 100644
--- a/packages/e2e-tests/specs/editor/various/undo.test.js
+++ b/packages/e2e-tests/specs/editor/various/undo.test.js
@@ -173,6 +173,7 @@ describe( 'undo', () => {
await page.keyboard.type( 'test' );
await saveDraft();
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
await page.click( '[data-type="core/paragraph"]' );
await pressKeyWithModifier( 'primary', 'a' );
await pressKeyWithModifier( 'primary', 'b' );
@@ -345,6 +346,7 @@ describe( 'undo', () => {
await page.keyboard.type( 'original' );
await saveDraft();
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// Issue is demonstrated by forcing state merges (multiple inputs) on
// an existing text after a fresh reload.
@@ -391,6 +393,7 @@ describe( 'undo', () => {
await page.keyboard.type( '1' );
await saveDraft();
await page.reload();
+ await page.waitForSelector( '.edit-post-layout' );
// Expect undo button to be disabled.
expect(
diff --git a/packages/e2e-tests/specs/editor/various/writing-flow.test.js b/packages/e2e-tests/specs/editor/various/writing-flow.test.js
index fe9df94bb2fbb..8b9ee8396257d 100644
--- a/packages/e2e-tests/specs/editor/various/writing-flow.test.js
+++ b/packages/e2e-tests/specs/editor/various/writing-flow.test.js
@@ -21,12 +21,15 @@ const addParagraphsAndColumnsDemo = async () => {
await page.keyboard.type( 'First paragraph' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '/columns' );
+ await page.waitForXPath(
+ `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Columns')]`
+ );
await page.keyboard.press( 'Enter' );
await page.click( ':focus [aria-label="Two columns; equal split"]' );
await page.click( ':focus .block-editor-button-block-appender' );
await page.waitForSelector( ':focus.block-editor-inserter__search-input' );
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( '1st col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "1st" instead of "First" here.
@@ -37,7 +40,7 @@ const addParagraphsAndColumnsDemo = async () => {
await page.click( ':focus .block-editor-button-block-appender' );
await page.waitForSelector( ':focus.block-editor-inserter__search-input' );
await page.keyboard.type( 'Paragraph' );
- await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
+ await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result.
await page.keyboard.press( 'Enter' ); // Insert paragraph.
await page.keyboard.type( '2nd col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "2nd" instead of "Second" here.
@@ -407,6 +410,17 @@ describe( 'Writing Flow', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+ it( 'should merge paragraphs', async () => {
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.press( 'ArrowLeft' );
+ await page.keyboard.press( 'Backspace' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
it( 'should merge forwards', async () => {
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '1' );
diff --git a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap
index c575b3297d2a2..eb7a6c163f2f3 100644
--- a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap
+++ b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap
@@ -36,11 +36,7 @@ exports[`Navigation Creating from existing Menus allows a navigation block to be
"
`;
-exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = `
-"
-
-"
-`;
+exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = `""`;
exports[`Navigation Creating from existing Menus does not display option to create from existing menus if there are no menus 1`] = `""`;
diff --git a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js
index 11de0b508a761..df8aa129a8e7d 100644
--- a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js
+++ b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js
@@ -11,6 +11,7 @@ import {
visitAdminPage,
createNewPost,
publishPost,
+ trashAllPosts,
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';
@@ -18,7 +19,6 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies
*/
import { useExperimentalFeatures } from '../../experimental-features';
-import { trashExistingPosts } from '../../config/setup-test-framework';
const visitSiteEditor = async () => {
const query = addQueryArgs( '', {
@@ -27,7 +27,7 @@ const visitSiteEditor = async () => {
await visitAdminPage( 'admin.php', query );
// Waits for the template part to load...
await page.waitForSelector(
- '.wp-block[data-type="core/template-part"] .block-editor-inner-blocks'
+ '.wp-block[data-type="core/template-part"] .block-editor-block-list__layout'
);
};
@@ -49,34 +49,32 @@ const getTemplateDropdownElement = async ( itemName ) => {
const createTemplatePart = async (
templatePartName = 'test-template-part',
- themeName = 'test-theme',
isNested = false
) => {
// Create new template part.
await insertBlock( 'Template Part' );
const [ createNewButton ] = await page.$x(
- '//button[contains(text(), "Create new")]'
+ '//button[contains(text(), "New template part")]'
);
await createNewButton.click();
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( templatePartName );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( themeName );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Enter' );
await page.waitForSelector(
isNested
- ? '.wp-block[data-type="core/template-part"] .wp-block[data-type="core/template-part"] .block-editor-inner-blocks'
- : '.wp-block[data-type="core/template-part"] .block-editor-inner-blocks'
+ ? '.wp-block[data-type="core/template-part"] .wp-block[data-type="core/template-part"] .block-editor-block-list__layout'
+ : '.wp-block[data-type="core/template-part"] .block-editor-block-list__layout'
);
+ await page.focus( '.wp-block-template-part__name-panel input' );
+ await page.keyboard.type( templatePartName );
};
const editTemplatePart = async ( textToAdd, isNested = false ) => {
await page.click(
- isNested
- ? '.wp-block[data-type="core/template-part"] .wp-block[data-type="core/template-part"]'
- : '.wp-block[data-type="core/template-part"]'
+ `${
+ isNested
+ ? '.wp-block[data-type="core/template-part"] .wp-block[data-type="core/template-part"]'
+ : '.wp-block[data-type="core/template-part"]'
+ } .block-editor-button-block-appender`
);
+ await page.click( '.editor-block-list-item-paragraph' );
for ( const text of textToAdd ) {
await page.keyboard.type( text );
await page.keyboard.press( 'Enter' );
@@ -166,8 +164,8 @@ describe( 'Multi-entity editor states', () => {
] );
beforeAll( async () => {
- await trashExistingPosts( 'wp_template' );
- await trashExistingPosts( 'wp_template_part' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
} );
it( 'should not display any dirty entities when loading the site editor', async () => {
@@ -196,8 +194,8 @@ describe( 'Multi-entity editor states', () => {
describe( 'Multi-entity edit', () => {
beforeAll( async () => {
- await trashExistingPosts( 'wp_template' );
- await trashExistingPosts( 'wp_template_part' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
await createNewPost( {
postType: 'wp_template',
title: kebabCase( templateName ),
@@ -208,7 +206,7 @@ describe( 'Multi-entity editor states', () => {
'Default template part test text.',
'Second paragraph test.',
] );
- await createTemplatePart( nestedTPName, 'test-theme', true );
+ await createTemplatePart( nestedTPName, true );
await editTemplatePart(
[ 'Nested Template Part Text.', 'Second Nested test.' ],
true
diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js
index 5dd1c1f14d83c..4acdae098a226 100644
--- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js
+++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js
@@ -6,6 +6,7 @@ import {
insertBlock,
publishPost,
visitAdminPage,
+ trashAllPosts,
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';
@@ -13,7 +14,6 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies
*/
import { useExperimentalFeatures } from '../../experimental-features';
-import { trashExistingPosts } from '../../config/setup-test-framework';
describe( 'Multi-entity save flow', () => {
// Selectors - usable between Post/Site editors.
@@ -21,10 +21,11 @@ describe( 'Multi-entity save flow', () => {
const checkboxInputSelector = '.components-checkbox-control__input';
const entitiesSaveSelector = '.editor-entities-saved-states__save-button';
const templatePartSelector = '*[data-type="core/template-part"]';
- const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-inner-blocks`;
+ const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-block-list__layout`;
const savePanelSelector = '.entities-saved-states__panel';
const closePanelButtonSelector = 'button[aria-label="Close panel"]';
- const createNewButtonSelector = '//button[contains(text(), "Create new")]';
+ const createNewButtonSelector =
+ '//button[contains(text(), "New template part")]';
// Reusable assertions across Post/Site editors.
const assertAllBoxesChecked = async () => {
@@ -58,8 +59,8 @@ describe( 'Multi-entity save flow', () => {
] );
beforeAll( async () => {
- await trashExistingPosts( 'wp_template' );
- await trashExistingPosts( 'wp_template_part' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
} );
describe( 'Post Editor', () => {
@@ -113,16 +114,13 @@ describe( 'Multi-entity save flow', () => {
createNewButtonSelector
);
await createNewButton.click();
+ await page.waitForSelector( activatedTemplatePartSelector );
await page.keyboard.press( 'Tab' );
await page.keyboard.type( 'test-template-part' );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( 'test-theme' );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Enter' );
// Make some changes in new Template Part.
- await page.waitForSelector( activatedTemplatePartSelector );
- await page.click( templatePartSelector );
+ await page.click( '.block-editor-button-block-appender' );
+ await page.click( '.editor-block-list-item-paragraph' );
await page.keyboard.type( 'some words...' );
await assertMultiSaveEnabled();
@@ -231,7 +229,7 @@ describe( 'Multi-entity save flow', () => {
// Ensure we are on 'front-page' demo template.
await page.click( templateDropdownSelector );
- const [ demoTemplateButton ] = await page.$x(
+ const demoTemplateButton = await page.waitForXPath(
demoTemplateSelector
);
await demoTemplateButton.click();
diff --git a/packages/e2e-tests/specs/experiments/navigation.test.js b/packages/e2e-tests/specs/experiments/navigation.test.js
index 26cc1c339d4db..7b2a81824de7a 100644
--- a/packages/e2e-tests/specs/experiments/navigation.test.js
+++ b/packages/e2e-tests/specs/experiments/navigation.test.js
@@ -9,6 +9,7 @@ import {
setUpResponseMocking,
clickBlockToolbarButton,
pressKeyWithModifier,
+ showBlockToolbar,
} from '@wordpress/e2e-test-utils';
/**
@@ -190,7 +191,7 @@ async function updateActiveNavigationLink( { url, label, type } ) {
// Wait for the autocomplete suggestion item to appear.
await page.waitForXPath( suggestionPath );
// Set the suggestion
- const [ suggestion ] = await page.$x( suggestionPath );
+ const suggestion = await page.waitForXPath( suggestionPath );
// Select it (so we're clicking the right one, even if it's further down the list)
await suggestion.click();
@@ -217,17 +218,13 @@ async function updateActiveNavigationLink( { url, label, type } ) {
}
async function selectDropDownOption( optionText ) {
- const buttonText = 'Select where to start from…';
- await page.waitForXPath(
- `//button[text()="${ buttonText }"][not(@disabled)]`
+ const selectToggle = await page.waitForSelector(
+ '.wp-block-navigation-placeholder__select-control button'
);
- const [ dropdownToggle ] = await page.$x(
- `//button[text()="${ buttonText }"][not(@disabled)]`
+ await selectToggle.click();
+ const theOption = await page.waitForXPath(
+ `//li[text()="${ optionText }"]`
);
- await dropdownToggle.click();
-
- const [ theOption ] = await page.$x( `//li[text()="${ optionText }"]` );
-
await theOption.click();
}
@@ -239,7 +236,7 @@ async function clickCreateButton() {
);
// Then locate...
- const [ createNavigationButton ] = await page.$x(
+ const createNavigationButton = await page.waitForXPath(
`//button[text()="${ buttonText }"][not(@disabled)]`
);
@@ -248,10 +245,21 @@ async function clickCreateButton() {
}
async function createEmptyNavBlock() {
- await selectDropDownOption( 'Create empty menu' );
+ await selectDropDownOption( 'Create empty Navigation' );
await clickCreateButton();
}
+async function addLinkBlock() {
+ // Using 'click' here checks for regressions of https://github.com/WordPress/gutenberg/issues/18329,
+ // an issue where the block appender requires two clicks.
+ await page.click( '.wp-block-navigation .block-list-appender' );
+
+ const [ linkButton ] = await page.$x(
+ "//*[contains(@class, 'block-editor-inserter__quick-inserter')]//*[text()='Link']"
+ );
+ await linkButton.click();
+}
+
beforeEach( async () => {
await createNewPost();
} );
@@ -282,7 +290,7 @@ describe( 'Navigation', () => {
// Add the navigation block.
await insertBlock( 'Navigation' );
- await selectDropDownOption( 'New from all top-level pages' );
+ await selectDropDownOption( 'Create from all top-level pages' );
await clickCreateButton();
@@ -297,27 +305,23 @@ describe( 'Navigation', () => {
// Add the navigation block.
await insertBlock( 'Navigation' );
- const dropdownButtonText = 'Select where to start from…';
- await page.waitForXPath(
- `//button[text()="${ dropdownButtonText }"][not(@disabled)]`
+ await page.waitForSelector(
+ '.wp-block-navigation-placeholder__select-control button'
);
- const [ dropdownToggle ] = await page.$x(
- `//button[text()="${ dropdownButtonText }"][not(@disabled)]`
+ await page.click(
+ '.wp-block-navigation-placeholder__select-control button'
);
- await dropdownToggle.click();
-
const dropDownItemsLength = await page.$$eval(
'ul[role="listbox"] li[role="option"]',
( els ) => els.length
);
// Should only be showing
- // 1. Placeholder value.
- // 2. Create empty menu.
- expect( dropDownItemsLength ).toEqual( 2 );
+ // 1. Create empty menu.
+ expect( dropDownItemsLength ).toEqual( 1 );
- await page.waitForXPath( '//li[text()="Create empty menu"]' );
+ await page.waitForXPath( '//li[text()="Create empty Navigation"]' );
// Snapshot should contain the mocked menu items.
expect( await getEditedPostContent() ).toMatchSnapshot();
@@ -335,12 +339,12 @@ describe( 'Navigation', () => {
await clickCreateButton();
- // await page.waitFor( 50000000 );
+ await page.waitForSelector( '.wp-block-navigation__container' );
// Scope element selector to the Editor's "Content" region as otherwise it picks up on
// block previews.
const navBlockItemsLength = await page.$$eval(
- '[aria-label="Content"][role="region"] li[aria-label="Block: Link"]',
+ '[aria-label="Editor content"][role="region"] li[aria-label="Block: Link"]',
( els ) => els.length
);
@@ -366,13 +370,12 @@ describe( 'Navigation', () => {
// Scope element selector to the "Editor content" as otherwise it picks up on
// Block Style live previews.
const navBlockItemsLength = await page.$$eval(
- '[aria-label="Content"][role="region"] li[aria-label="Block: Link"]',
+ '[aria-label="Editor content"][role="region"] li[aria-label="Block: Link"]',
( els ) => els.length
);
// Assert an empty Nav Block is created.
- // We expect 1 here because a "placeholder" Nav Item Block is automatically inserted
- expect( navBlockItemsLength ).toEqual( 1 );
+ expect( navBlockItemsLength ).toEqual( 0 );
// Snapshot should contain the mocked menu items.
expect( await getEditedPostContent() ).toMatchSnapshot();
@@ -385,14 +388,12 @@ describe( 'Navigation', () => {
// Add the navigation block.
await insertBlock( 'Navigation' );
- const dropdownButtonText = 'Select where to start from…';
- await page.waitForXPath(
- `//button[text()="${ dropdownButtonText }"][not(@disabled)]`
+ await page.waitForSelector(
+ '.wp-block-navigation-placeholder__select-control button'
);
- const [ dropdownToggle ] = await page.$x(
- `//button[text()="${ dropdownButtonText }"][not(@disabled)]`
+ await page.click(
+ '.wp-block-navigation-placeholder__select-control button'
);
- await dropdownToggle.click();
const dropDownItemsLength = await page.$$eval(
'ul[role="listbox"] li[role="option"]',
@@ -400,11 +401,10 @@ describe( 'Navigation', () => {
);
// Should only be showing
- // 1. Placeholder.
- // 2. Create from Empty.
- expect( dropDownItemsLength ).toEqual( 2 );
+ // 1. Create empty menu.
+ expect( dropDownItemsLength ).toEqual( 1 );
- await page.waitForXPath( '//li[text()="Create empty menu"]' );
+ await page.waitForXPath( '//li[text()="Create empty Navigation"]' );
// Snapshot should contain the mocked menu items.
expect( await getEditedPostContent() ).toMatchSnapshot();
@@ -420,20 +420,18 @@ describe( 'Navigation', () => {
await createEmptyNavBlock();
- // Add a link to the default Link block.
+ await addLinkBlock();
+
+ // Add a link to the Link block.
await updateActiveNavigationLink( {
url: 'https://wordpress.org',
label: 'WP',
type: 'url',
} );
- // Move the mouse to reveal the block movers. Without this the test seems to fail.
- await page.mouse.move( 100, 100 );
+ await showBlockToolbar();
- // Add another Link block.
- // Using 'click' here checks for regressions of https://github.com/WordPress/gutenberg/issues/18329,
- // an issue where the block appender requires two clicks.
- await page.click( '.wp-block-navigation .block-list-appender' );
+ await addLinkBlock();
// After adding a new block, search input should be shown immediately.
// Verify that Escape would close the popover.
@@ -501,6 +499,8 @@ describe( 'Navigation', () => {
// Create an empty nav block.
await createEmptyNavBlock();
+ await addLinkBlock();
+
// Wait for URL input to be focused
await page.waitForSelector(
'input.block-editor-url-input__input:focus'
diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js
index de9e15cbc8dc8..835ea93a0a7c0 100644
--- a/packages/e2e-tests/specs/experiments/template-part.test.js
+++ b/packages/e2e-tests/specs/experiments/template-part.test.js
@@ -6,6 +6,7 @@ import {
insertBlock,
disablePrePublishChecks,
visitAdminPage,
+ trashAllPosts,
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';
@@ -13,7 +14,6 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies
*/
import { useExperimentalFeatures } from '../../experimental-features';
-import { trashExistingPosts } from '../../config/setup-test-framework';
describe( 'Template Part', () => {
useExperimentalFeatures( [
@@ -22,23 +22,24 @@ describe( 'Template Part', () => {
] );
beforeAll( async () => {
- await trashExistingPosts( 'wp_template' );
- await trashExistingPosts( 'wp_template_part' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
} );
afterAll( async () => {
- await trashExistingPosts( 'wp_template' );
- await trashExistingPosts( 'wp_template_part' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
} );
describe( 'Template part block', () => {
- beforeEach( () =>
- visitAdminPage(
+ beforeEach( async () => {
+ await visitAdminPage(
'admin.php',
addQueryArgs( '', {
page: 'gutenberg-edit-site',
} ).slice( 1 )
- )
- );
+ );
+ await page.waitForSelector( '.edit-site-visual-editor' );
+ } );
it( 'Should load customizations when in a template even if only the slug and theme attributes are set.', async () => {
// Switch to editing the header template part.
@@ -80,8 +81,6 @@ describe( 'Template Part', () => {
describe( 'Template part placeholder', () => {
// Test constants for template part.
- const testSlug = 'test-template-part';
- const testTheme = 'test-theme';
const testContent = 'some words...';
// Selectors
@@ -89,12 +88,12 @@ describe( 'Template Part', () => {
'.editor-entities-saved-states__save-button';
const savePostSelector = '.editor-post-publish-button__button';
const templatePartSelector = '*[data-type="core/template-part"]';
- const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-inner-blocks`;
+ const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-block-list__layout`;
const testContentSelector = `//p[contains(., "${ testContent }")]`;
const createNewButtonSelector =
- '//button[contains(text(), "Create new")]';
- const disabledButtonSelector =
- '.wp-block-template-part__placeholder-create-button[disabled]';
+ '//button[contains(text(), "New template part")]';
+ const chooseExistingButtonSelector =
+ '//button[contains(text(), "Choose existing")]';
it( 'Should insert new template part on creation', async () => {
await createNewPost();
@@ -105,12 +104,6 @@ describe( 'Template Part', () => {
createNewButtonSelector
);
await createNewButton.click();
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( testSlug );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( testTheme );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Enter' );
const newTemplatePart = await page.waitForSelector(
activatedTemplatePartSelector
@@ -118,7 +111,8 @@ describe( 'Template Part', () => {
expect( newTemplatePart ).toBeTruthy();
// Finish creating template part, insert some text, and save.
- await page.click( templatePartSelector );
+ await page.click( '.block-editor-button-block-appender' );
+ await page.click( '.editor-block-list-item-paragraph' );
await page.keyboard.type( testContent );
await page.click( savePostSelector );
await page.click( entitiesSaveSelector );
@@ -128,6 +122,10 @@ describe( 'Template Part', () => {
await createNewPost();
// Try to insert the template part we created.
await insertBlock( 'Template Part' );
+ const [ chooseExistingButton ] = await page.$x(
+ chooseExistingButtonSelector
+ );
+ await chooseExistingButton.click();
const preview = await page.waitForXPath( testContentSelector );
expect( preview ).toBeTruthy();
} );
@@ -141,24 +139,5 @@ describe( 'Template Part', () => {
);
expect( templatePartContent ).toBeTruthy();
} );
-
- it( 'Should disable create button for slug/theme combo', async () => {
- await createNewPost();
- // Create new template part.
- await insertBlock( 'Template Part' );
- const [ createNewButton ] = await page.$x(
- createNewButtonSelector
- );
- await createNewButton.click();
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( testSlug );
- await page.keyboard.press( 'Tab' );
- await page.keyboard.type( testTheme );
-
- const disabledButton = await page.waitForSelector(
- disabledButtonSelector
- );
- expect( disabledButton ).toBeTruthy();
- } );
} );
} );
diff --git a/packages/e2e-tests/specs/performance/performance.test.js b/packages/e2e-tests/specs/performance/post-editor.test.js
similarity index 94%
rename from packages/e2e-tests/specs/performance/performance.test.js
rename to packages/e2e-tests/specs/performance/post-editor.test.js
index 9360c98a6d313..844ee6b9c0729 100644
--- a/packages/e2e-tests/specs/performance/performance.test.js
+++ b/packages/e2e-tests/specs/performance/post-editor.test.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { join } from 'path';
+import { basename, join } from 'path';
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
/**
@@ -71,7 +71,7 @@ function getSelectionEventDurations( trace ) {
jest.setTimeout( 1000000 );
-describe( 'Performance', () => {
+describe( 'Post Editor Performance', () => {
it( 'Loading, typing and selecting blocks', async () => {
const results = {
load: [],
@@ -123,6 +123,7 @@ describe( 'Performance', () => {
}
// Measuring typing performance
+ await page.waitForSelector( '.edit-post-layout' );
await insertBlock( 'Paragraph' );
i = 200;
const traceFile = __dirname + '/trace.json';
@@ -182,8 +183,10 @@ describe( 'Performance', () => {
const [ focusEvents ] = getSelectionEventDurations( traceResults );
results.focus = focusEvents;
+ const resultsFilename = basename( __filename, '.js' ) + '.results.json';
+
writeFileSync(
- __dirname + '/results.json',
+ join( __dirname, resultsFilename ),
JSON.stringify( results, null, 2 )
);
diff --git a/packages/e2e-tests/specs/performance/site-editor.test.js b/packages/e2e-tests/specs/performance/site-editor.test.js
new file mode 100644
index 0000000000000..da62661f07d88
--- /dev/null
+++ b/packages/e2e-tests/specs/performance/site-editor.test.js
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import { basename, join } from 'path';
+import { writeFileSync } from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import { useExperimentalFeatures } from '../../experimental-features';
+
+/**
+ * WordPress dependencies
+ */
+import { trashAllPosts, visitAdminPage } from '@wordpress/e2e-test-utils';
+import { addQueryArgs } from '@wordpress/url';
+
+jest.setTimeout( 1000000 );
+
+describe( 'Site Editor Performance', () => {
+ useExperimentalFeatures( [
+ '#gutenberg-full-site-editing',
+ '#gutenberg-full-site-editing-demo',
+ ] );
+
+ beforeAll( async () => {
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
+ } );
+ afterAll( async () => {
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
+ } );
+
+ it( 'Loading', async () => {
+ const results = {
+ load: [],
+ domcontentloaded: [],
+ type: [],
+ focus: [],
+ };
+
+ await visitAdminPage(
+ 'admin.php',
+ addQueryArgs( '', {
+ page: 'gutenberg-edit-site',
+ } ).slice( 1 )
+ );
+
+ let i = 3;
+
+ // Measuring loading time
+ while ( i-- ) {
+ await page.reload( { waitUntil: [ 'domcontentloaded', 'load' ] } );
+ const timings = JSON.parse(
+ await page.evaluate( () =>
+ JSON.stringify( window.performance.timing )
+ )
+ );
+ const {
+ navigationStart,
+ domContentLoadedEventEnd,
+ loadEventEnd,
+ } = timings;
+ results.load.push( loadEventEnd - navigationStart );
+ results.domcontentloaded.push(
+ domContentLoadedEventEnd - navigationStart
+ );
+ }
+
+ const resultsFilename = basename( __filename, '.js' ) + '.results.json';
+
+ writeFileSync(
+ join( __dirname, resultsFilename ),
+ JSON.stringify( results, null, 2 )
+ );
+
+ expect( true ).toBe( true );
+ } );
+} );
diff --git a/packages/edit-navigation/package.json b/packages/edit-navigation/package.json
index 169d803b03737..08bc66b087089 100644
--- a/packages/edit-navigation/package.json
+++ b/packages/edit-navigation/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/edit-navigation",
- "version": "1.1.0",
+ "version": "1.3.3",
"private": true,
"description": "Module for the Navigation page in WordPress.",
"author": "The WordPress Contributors",
@@ -35,6 +35,7 @@
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
+ "@wordpress/core-data": "file:../core-data",
"@wordpress/data": "file:../data",
"@wordpress/data-controls": "file:../data-controls",
"@wordpress/dom-ready": "file:../dom-ready",
@@ -42,13 +43,15 @@
"@wordpress/hooks": "file:../hooks",
"@wordpress/html-entities": "file:../html-entities",
"@wordpress/i18n": "file:../i18n",
+ "@wordpress/icons": "file:../icons",
"@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts",
"@wordpress/media-utils": "file:../media-utils",
"@wordpress/notices": "file:../notices",
"@wordpress/url": "file:../url",
"classnames": "^2.2.5",
- "lodash": "^4.17.15",
- "rememo": "^3.0.0"
+ "lodash": "^4.17.19",
+ "rememo": "^3.0.0",
+ "uuid": "^7.0.2"
},
"publishConfig": {
"access": "public"
diff --git a/packages/edit-navigation/src/components/delete-menu-button/index.js b/packages/edit-navigation/src/components/delete-menu-button/index.js
index 118b1cc972fd4..9cf1799ebfaf7 100644
--- a/packages/edit-navigation/src/components/delete-menu-button/index.js
+++ b/packages/edit-navigation/src/components/delete-menu-button/index.js
@@ -1,20 +1,10 @@
/**
* WordPress dependencies
*/
-import apiFetch from '@wordpress/api-fetch';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-export default function DeleteMenuButton( { menuId, onDelete } ) {
- const deleteMenu = async ( recordId ) => {
- const path = `/__experimental/menus/${ recordId }?force=true`;
- const deletedRecord = await apiFetch( {
- path,
- method: 'DELETE',
- } );
- return deletedRecord.previous;
- };
-
+export default function DeleteMenuButton( { onDelete } ) {
const askToDelete = async () => {
if (
// eslint-disable-next-line no-alert
@@ -22,8 +12,7 @@ export default function DeleteMenuButton( { menuId, onDelete } ) {
__( 'Are you sure you want to delete this navigation?' )
)
) {
- const deletedMenu = await deleteMenu( menuId );
- onDelete( deletedMenu.id );
+ onDelete();
}
};
diff --git a/packages/edit-navigation/src/components/error-boundary/index.js b/packages/edit-navigation/src/components/error-boundary/index.js
new file mode 100644
index 0000000000000..d466e3e7c4acf
--- /dev/null
+++ b/packages/edit-navigation/src/components/error-boundary/index.js
@@ -0,0 +1,53 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { Button } from '@wordpress/components';
+import { Warning } from '@wordpress/block-editor';
+
+class ErrorBoundary extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.reboot = this.reboot.bind( this );
+
+ this.state = {
+ error: null,
+ };
+ }
+
+ componentDidCatch( error ) {
+ this.setState( { error } );
+ }
+
+ reboot() {
+ if ( this.props.onError ) {
+ this.props.onError();
+ }
+ }
+
+ render() {
+ const { error } = this.state;
+ if ( ! error ) {
+ return this.props.children;
+ }
+
+ return (
+
+ { __( 'Attempt Recovery' ) }
+ ,
+ ] }
+ >
+ { __(
+ 'The navigation editor has encountered an unexpected error.'
+ ) }
+
+ );
+ }
+}
+
+export default ErrorBoundary;
diff --git a/packages/edit-navigation/src/components/error-boundary/style.scss b/packages/edit-navigation/src/components/error-boundary/style.scss
new file mode 100644
index 0000000000000..4f8b095dc6e89
--- /dev/null
+++ b/packages/edit-navigation/src/components/error-boundary/style.scss
@@ -0,0 +1,7 @@
+.navigation-editor-error-boundary {
+ margin: auto;
+ max-width: 780px;
+ padding: 20px;
+ margin-top: 60px;
+ box-shadow: $shadow-modal;
+}
diff --git a/packages/edit-navigation/src/components/layout/index.js b/packages/edit-navigation/src/components/layout/index.js
index fdfcdd0e9b2f2..780afd26e22eb 100644
--- a/packages/edit-navigation/src/components/layout/index.js
+++ b/packages/edit-navigation/src/components/layout/index.js
@@ -16,46 +16,49 @@ import { __ } from '@wordpress/i18n';
import Notices from '../notices';
import MenusEditor from '../menus-editor';
import MenuLocationsEditor from '../menu-locations-editor';
+import ErrorBoundary from '../error-boundary';
export default function Layout( { blockEditorSettings } ) {
return (
<>
-
-
-
-
-
- { ( tab ) => (
- <>
- { tab.name === 'menus' && (
-
- ) }
- { tab.name === 'menu-locations' && (
-
- ) }
- >
- ) }
-
-
-
-
-
+
+
+
+
+
+
+ { ( tab ) => (
+
+ { tab.name === 'menus' && (
+
+ ) }
+ { tab.name === 'menu-locations' && (
+
+ ) }
+
+ ) }
+
+
+
+
+
+
>
);
}
diff --git a/packages/edit-navigation/src/components/menu-editor/block-editor-area.js b/packages/edit-navigation/src/components/menu-editor/block-editor-area.js
deleted file mode 100644
index 9c06ba2c8776e..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/block-editor-area.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
-/**
- * WordPress dependencies
- */
-import {
- BlockList,
- BlockToolbar,
- NavigableToolbar,
- ObserveTyping,
- WritingFlow,
-} from '@wordpress/block-editor';
-import { useEffect } from '@wordpress/element';
-import {
- Button,
- Card,
- CardHeader,
- CardBody,
- CardFooter,
- Popover,
-} from '@wordpress/components';
-import { useSelect, useDispatch } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import DeleteMenuButton from '../delete-menu-button';
-
-export default function BlockEditorArea( {
- onDeleteMenu,
- menuId,
- saveBlocks,
-} ) {
- const { rootBlockId, isNavigationModeActive, hasSelectedBlock } = useSelect(
- ( select ) => {
- const {
- isNavigationMode,
- getBlockSelectionStart,
- getBlock,
- getBlocks,
- } = select( 'core/block-editor' );
-
- const selectionStartClientId = getBlockSelectionStart();
-
- return {
- rootBlockId: getBlocks()[ 0 ]?.clientId,
- isNavigationModeActive: isNavigationMode(),
- hasSelectedBlock:
- !! selectionStartClientId &&
- !! getBlock( selectionStartClientId ),
- };
- },
- []
- );
-
- // Select the navigation block when it becomes available
- const { selectBlock } = useDispatch( 'core/block-editor' );
- useEffect( () => {
- if ( rootBlockId ) {
- selectBlock( rootBlockId );
- }
- }, [ rootBlockId ] );
-
- return (
-
-
-
- { __( 'Navigation menu' ) }
-
-
-
-
-
-
- { hasSelectedBlock && }
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/edit-navigation/src/components/menu-editor/index.js b/packages/edit-navigation/src/components/menu-editor/index.js
deleted file mode 100644
index ce40b46c4f6ae..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/index.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- BlockEditorKeyboardShortcuts,
- BlockEditorProvider,
-} from '@wordpress/block-editor';
-import { useViewportMatch } from '@wordpress/compose';
-import { useMemo } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import useMenuItems from './use-menu-items';
-import useNavigationBlocks from './use-navigation-blocks';
-import MenuEditorShortcuts from './shortcuts';
-import BlockEditorArea from './block-editor-area';
-import NavigationStructureArea from './navigation-structure-area';
-
-export default function MenuEditor( {
- menuId,
- blockEditorSettings,
- onDeleteMenu,
-} ) {
- const isLargeViewport = useViewportMatch( 'medium' );
- const query = useMemo( () => ( { menus: menuId, per_page: -1 } ), [
- menuId,
- ] );
- const {
- menuItems,
- eventuallySaveMenuItems,
- createMissingMenuItems,
- } = useMenuItems( query );
- const { blocks, setBlocks, menuItemsRef } = useNavigationBlocks(
- menuItems
- );
- const saveMenuItems = () => eventuallySaveMenuItems( blocks, menuItemsRef );
-
- return (
-
-
-
-
- setBlocks( updatedBlocks ) }
- onChange={ ( updatedBlocks ) => {
- createMissingMenuItems( updatedBlocks, menuItemsRef );
- setBlocks( updatedBlocks );
- } }
- settings={ {
- ...blockEditorSettings,
- templateLock: 'all',
- hasFixedToolbar: true,
- } }
- >
-
-
-
-
-
-
- );
-}
diff --git a/packages/edit-navigation/src/components/menu-editor/promise-queue.js b/packages/edit-navigation/src/components/menu-editor/promise-queue.js
deleted file mode 100644
index d560164e91e76..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/promise-queue.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * A concurrency primitive that runs at most `concurrency` async tasks at once.
- */
-export default class PromiseQueue {
- constructor( concurrency = 1 ) {
- this.concurrency = concurrency;
- this.queue = [];
- this.active = [];
- this.listeners = [];
- }
-
- enqueue( action ) {
- this.queue.push( action );
- this.run();
- }
-
- run() {
- while ( this.queue.length && this.active.length <= this.concurrency ) {
- const action = this.queue.shift();
- const promise = action().then( () => {
- this.active.splice( this.active.indexOf( promise ), 1 );
- this.run();
- this.notifyIfEmpty();
- } );
- this.active.push( promise );
- }
- }
-
- notifyIfEmpty() {
- if ( this.active.length === 0 && this.queue.length === 0 ) {
- for ( const l of this.listeners ) {
- l();
- }
- this.listeners = [];
- }
- }
-
- /**
- * Calls `callback` once all async actions in the queue are finished,
- * or immediately if no actions are running.
- *
- * @param {Function} callback Callback to call
- */
- then( callback ) {
- if ( this.active.length ) {
- this.listeners.push( callback );
- } else {
- callback();
- }
- }
-}
diff --git a/packages/edit-navigation/src/components/menu-editor/shortcuts.js b/packages/edit-navigation/src/components/menu-editor/shortcuts.js
deleted file mode 100644
index 96889b67a8daa..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/shortcuts.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { useEffect, useCallback } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
-import { useShortcut } from '@wordpress/keyboard-shortcuts';
-import { __ } from '@wordpress/i18n';
-
-function MenuEditorShortcuts( { saveBlocks } ) {
- useShortcut(
- 'core/edit-navigation/save-menu',
- useCallback( ( event ) => {
- event.preventDefault();
- saveBlocks();
- } ),
- {
- bindGlobal: true,
- }
- );
-
- return null;
-}
-
-function RegisterMenuEditorShortcuts() {
- const { registerShortcut } = useDispatch( 'core/keyboard-shortcuts' );
- useEffect( () => {
- registerShortcut( {
- name: 'core/edit-navigation/save-menu',
- category: 'global',
- description: __( 'Save the menu currently being edited.' ),
- keyCombination: {
- modifier: 'primary',
- character: 's',
- },
- } );
- }, [ registerShortcut ] );
-
- return null;
-}
-
-MenuEditorShortcuts.Register = RegisterMenuEditorShortcuts;
-
-export default MenuEditorShortcuts;
diff --git a/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js b/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js
deleted file mode 100644
index b21fa5416d1e6..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * WordPress dependencies
- */
-import apiFetch from '@wordpress/api-fetch';
-import { useRef, useCallback } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import { flattenBlocks } from './helpers';
-import PromiseQueue from './promise-queue';
-
-/**
- * When a new Navigation child block is added, we create a draft menuItem for it because
- * the batch save endpoint expects all the menu items to have a valid id already.
- * PromiseQueue is used in order to
- * 1) limit the amount of requests processed at the same time
- * 2) save the menu only after all requests are finalized
- *
- * @return {function(*=): void} Function registering it's argument to be called once all menuItems are created.
- */
-export default function useCreateMissingMenuItems() {
- const promiseQueueRef = useRef( new PromiseQueue() );
- const enqueuedBlocksIds = useRef( [] );
- const createMissingMenuItems = ( blocks, menuItemsRef ) => {
- for ( const { clientId, name } of flattenBlocks( blocks ) ) {
- // No need to create menuItems for the wrapping navigation block
- if ( name === 'core/navigation' ) {
- continue;
- }
- // Menu item was already created
- if ( clientId in menuItemsRef.current ) {
- continue;
- }
- // Menu item already in the queue
- if ( enqueuedBlocksIds.current.includes( clientId ) ) {
- continue;
- }
- enqueuedBlocksIds.current.push( clientId );
- promiseQueueRef.current.enqueue( () =>
- createDraftMenuItem( clientId ).then( ( menuItem ) => {
- menuItemsRef.current[ clientId ] = menuItem;
- enqueuedBlocksIds.current.splice(
- enqueuedBlocksIds.current.indexOf( clientId )
- );
- } )
- );
- }
- };
- const onCreated = useCallback(
- ( callback ) => promiseQueueRef.current.then( callback ),
- [ promiseQueueRef.current ]
- );
- return { createMissingMenuItems, onCreated };
-}
-
-function createDraftMenuItem() {
- return apiFetch( {
- path: `/__experimental/menu-items`,
- method: 'POST',
- data: {
- title: 'Placeholder',
- url: 'Placeholder',
- menu_order: 0,
- },
- } );
-}
diff --git a/packages/edit-navigation/src/components/menu-editor/use-menu-items.js b/packages/edit-navigation/src/components/menu-editor/use-menu-items.js
deleted file mode 100644
index 250b9927153ab..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/use-menu-items.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- * External dependencies
- */
-import { keyBy, omit } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { useDispatch, useSelect } from '@wordpress/data';
-import { useEffect, useState } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-import apiFetch from '@wordpress/api-fetch';
-
-/**
- * Internal dependencies
- */
-import useCreateMissingMenuItems from './use-create-missing-menu-items';
-
-export default function useMenuItems( query ) {
- const menuItems = useFetchMenuItems( query );
- const saveMenuItems = useSaveMenuItems( query );
- const { createMissingMenuItems, onCreated } = useCreateMissingMenuItems();
- const eventuallySaveMenuItems = ( blocks, menuItemsRef ) =>
- onCreated( () => saveMenuItems( blocks, menuItemsRef ) );
- return { menuItems, eventuallySaveMenuItems, createMissingMenuItems };
-}
-
-export function useFetchMenuItems( query ) {
- const { menuItems, isResolving } = useSelect( ( select ) => ( {
- menuItems: select( 'core' ).getMenuItems( query ),
- isResolving: select( 'core/data' ).isResolving(
- 'core',
- 'getMenuItems',
- [ query ]
- ),
- } ) );
-
- const [ resolvedMenuItems, setResolvedMenuItems ] = useState( null );
-
- useEffect( () => {
- if ( isResolving || menuItems === null ) {
- return;
- }
-
- setResolvedMenuItems( menuItems );
- }, [ isResolving, menuItems ] );
-
- return resolvedMenuItems;
-}
-
-export function useSaveMenuItems( query ) {
- const { receiveEntityRecords } = useDispatch( 'core' );
- const { createSuccessNotice, createErrorNotice } = useDispatch(
- 'core/notices'
- );
-
- const saveBlocks = async ( blocks, menuItemsRef ) => {
- const result = await batchSave(
- query.menus,
- menuItemsRef,
- blocks[ 0 ]
- );
-
- if ( result.success ) {
- receiveEntityRecords( 'root', 'menuItem', [], query, true );
- createSuccessNotice( __( 'Navigation saved.' ), {
- type: 'snackbar',
- } );
- } else {
- createErrorNotice( __( 'There was an error.' ), {
- type: 'snackbar',
- } );
- }
- };
-
- return saveBlocks;
-}
-
-async function batchSave( menuId, menuItemsRef, navigationBlock ) {
- const { nonce, stylesheet } = await apiFetch( {
- path: '/__experimental/customizer-nonces/get-save-nonce',
- } );
-
- // eslint-disable-next-line no-undef
- const body = new FormData();
- body.append( 'wp_customize', 'on' );
- body.append( 'customize_theme', stylesheet );
- body.append( 'nonce', nonce );
- body.append( 'customize_changeset_uuid', uuidv4() );
- body.append( 'customize_autosaved', 'on' );
- body.append( 'customize_changeset_status', 'publish' );
- body.append( 'action', 'customize_save' );
- body.append(
- 'customized',
- computeCustomizedAttribute(
- navigationBlock.innerBlocks,
- menuId,
- menuItemsRef
- )
- );
-
- return await apiFetch( {
- url: '/wp-admin/admin-ajax.php',
- method: 'POST',
- body,
- } );
-}
-
-function computeCustomizedAttribute( blocks, menuId, menuItemsRef ) {
- const blocksList = blocksTreeToFlatList( blocks );
- const dataList = blocksList.map( ( { block, parentId, position } ) =>
- linkBlockToRequestItem( block, parentId, position )
- );
-
- // Create an object like { "nav_menu_item[12]": {...}} }
- const computeKey = ( item ) => `nav_menu_item[${ item.id }]`;
- const dataObject = keyBy( dataList, computeKey );
-
- // Deleted menu items should be sent as false, e.g. { "nav_menu_item[13]": false }
- for ( const clientId in menuItemsRef.current ) {
- const key = computeKey( menuItemsRef.current[ clientId ] );
- if ( ! ( key in dataObject ) ) {
- dataObject[ key ] = false;
- }
- }
-
- return JSON.stringify( dataObject );
-
- function blocksTreeToFlatList( innerBlocks, parentId = 0 ) {
- return innerBlocks.flatMap( ( block, index ) =>
- [ { block, parentId, position: index + 1 } ].concat(
- blocksTreeToFlatList(
- block.innerBlocks,
- getMenuItemForBlock( block )?.id
- )
- )
- );
- }
-
- function linkBlockToRequestItem( block, parentId, position ) {
- const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' );
- return {
- ...menuItem,
- position,
- title: block.attributes?.label,
- url: block.attributes.url,
- original_title: '',
- classes: ( menuItem.classes || [] ).join( ' ' ),
- xfn: ( menuItem.xfn || [] ).join( ' ' ),
- nav_menu_term_id: menuId,
- menu_item_parent: parentId,
- status: 'publish',
- _invalid: false,
- };
- }
-
- function getMenuItemForBlock( block ) {
- return omit( menuItemsRef.current[ block.clientId ] || {}, '_links' );
- }
-}
-
-function uuidv4() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, ( c ) => {
- // eslint-disable-next-line no-restricted-syntax
- const a = Math.random() * 16;
- // eslint-disable-next-line no-bitwise
- const r = a | 0;
- // eslint-disable-next-line no-bitwise
- const v = c === 'x' ? r : ( r & 0x3 ) | 0x8;
- return v.toString( 16 );
- } );
-}
diff --git a/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js b/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js
deleted file mode 100644
index 1b7899737fd78..0000000000000
--- a/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * External dependencies
- */
-import { keyBy, groupBy, sortBy } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { createBlock } from '@wordpress/blocks';
-import { useState, useRef, useEffect } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import { flattenBlocks } from './helpers';
-
-export default function useNavigationBlocks( menuItems ) {
- const [ blocks, setBlocks ] = useState( [] );
- const menuItemsRef = useRef( {} );
-
- // Refresh our model whenever menuItems change
- useEffect( () => {
- const [ innerBlocks, clientIdToMenuItemMapping ] = menuItemsToBlocks(
- menuItems,
- blocks[ 0 ]?.innerBlocks,
- menuItemsRef.current
- );
-
- const navigationBlock = blocks[ 0 ]
- ? { ...blocks[ 0 ], innerBlocks }
- : createBlock( 'core/navigation', {}, innerBlocks );
-
- setBlocks( [ navigationBlock ] );
- menuItemsRef.current = clientIdToMenuItemMapping;
- }, [ menuItems ] );
-
- return {
- blocks,
- setBlocks,
- menuItemsRef,
- };
-}
-
-const menuItemsToBlocks = (
- menuItems,
- prevBlocks = [],
- prevClientIdToMenuItemMapping = {}
-) => {
- const blocksByMenuId = mapBlocksByMenuId(
- prevBlocks,
- prevClientIdToMenuItemMapping
- );
-
- const itemsByParentID = groupBy( menuItems, 'parent' );
- const clientIdToMenuItemMapping = {};
- const menuItemsToTreeOfBlocks = ( items ) => {
- const innerBlocks = [];
- if ( ! items ) {
- return;
- }
-
- const sortedItems = sortBy( items, 'menu_order' );
- for ( const item of sortedItems ) {
- let menuItemInnerBlocks = [];
- if ( itemsByParentID[ item.id ]?.length ) {
- menuItemInnerBlocks = menuItemsToTreeOfBlocks(
- itemsByParentID[ item.id ]
- );
- }
- const linkBlock = menuItemToLinkBlock(
- item,
- menuItemInnerBlocks,
- blocksByMenuId[ item.id ]
- );
- clientIdToMenuItemMapping[ linkBlock.clientId ] = item;
- innerBlocks.push( linkBlock );
- }
- return innerBlocks;
- };
-
- // menuItemsToTreeOfLinkBlocks takes an array of top-level menu items and recursively creates all their innerBlocks
- const blocks = menuItemsToTreeOfBlocks( itemsByParentID[ 0 ] || [] );
- return [ blocks, clientIdToMenuItemMapping ];
-};
-
-function menuItemToLinkBlock(
- menuItem,
- innerBlocks = [],
- existingBlock = null
-) {
- const attributes = {
- label: menuItem.title.rendered,
- url: menuItem.url,
- };
-
- if ( existingBlock ) {
- return {
- ...existingBlock,
- attributes,
- innerBlocks,
- };
- }
- return createBlock( 'core/navigation-link', attributes, innerBlocks );
-}
-
-const mapBlocksByMenuId = ( blocks, menuItemsByClientId ) => {
- const blocksByClientId = keyBy( flattenBlocks( blocks ), 'clientId' );
- const blocksByMenuId = {};
- for ( const clientId in menuItemsByClientId ) {
- const menuItem = menuItemsByClientId[ clientId ];
- blocksByMenuId[ menuItem.id ] = blocksByClientId[ clientId ];
- }
- return blocksByMenuId;
-};
diff --git a/packages/edit-navigation/src/components/menus-editor/index.js b/packages/edit-navigation/src/components/menus-editor/index.js
index 04507772174cb..7b22b5a295c71 100644
--- a/packages/edit-navigation/src/components/menus-editor/index.js
+++ b/packages/edit-navigation/src/components/menus-editor/index.js
@@ -1,8 +1,13 @@
+/**
+ * External dependencies
+ */
+import { uniqueId } from 'lodash';
+
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
-import { useState, useEffect } from '@wordpress/element';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useState, useEffect, useRef } from '@wordpress/element';
import {
Button,
Card,
@@ -11,41 +16,73 @@ import {
SelectControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+const { DOMParser } = window;
/**
* Internal dependencies
*/
import CreateMenuArea from './create-menu-area';
-import MenuEditor from '../menu-editor';
+import NavigationEditor from '../navigation-editor';
export default function MenusEditor( { blockEditorSettings } ) {
- const { menus, hasLoadedMenus } = useSelect( ( select ) => {
- const { getMenus, hasFinishedResolution } = select( 'core' );
- const query = { per_page: -1 };
- return {
- menus: getMenus( query ),
- hasLoadedMenus: hasFinishedResolution( 'getMenus', [ query ] ),
- };
- }, [] );
-
+ const [ menuId, setMenuId ] = useState();
+ const [ showCreateMenuPanel, setShowCreateMenuPanel ] = useState( false );
const [ hasCompletedFirstLoad, setHasCompletedFirstLoad ] = useState(
false
);
+ const noticeId = useRef();
+
+ const { menus, hasLoadedMenus, menuDeleteError } = useSelect(
+ ( select ) => {
+ const {
+ getMenus,
+ hasFinishedResolution,
+ getLastEntityDeleteError,
+ } = select( 'core' );
+ const query = { per_page: -1 };
+ return {
+ menus: getMenus( query ),
+ hasLoadedMenus: hasFinishedResolution( 'getMenus', [ query ] ),
+ menuDeleteError: getLastEntityDeleteError(
+ 'root',
+ 'menu',
+ menuId
+ ),
+ };
+ },
+ [ menuId ]
+ );
+
+ const { deleteMenu } = useDispatch( 'core' );
+ const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
+
useEffect( () => {
if ( ! hasCompletedFirstLoad && hasLoadedMenus ) {
setHasCompletedFirstLoad( true );
}
}, [ hasLoadedMenus ] );
- const [ menuId, setMenuId ] = useState();
- const [ stateMenus, setStateMenus ] = useState();
- const [ showCreateMenuPanel, setShowCreateMenuPanel ] = useState( false );
+ // Handle REST API Error messages.
+ useEffect( () => {
+ if ( menuDeleteError ) {
+ // Error messages from the REST API often contain HTML.
+ // createErrorNotice does not support HTML in error text, so first
+ // strip HTML out using DOMParser.
+ const document = new DOMParser().parseFromString(
+ menuDeleteError.message,
+ 'text/html'
+ );
+ const errorText = document.body.textContent || '';
+ noticeId.current = uniqueId(
+ 'navigation-editor/menu-editor/edit-navigation-delete-menu-error'
+ );
+ createErrorNotice( errorText, { id: noticeId.current } );
+ }
+ }, [ menuDeleteError ] );
useEffect( () => {
if ( menus?.length ) {
- setStateMenus( menus );
-
// Only set menuId if it's currently unset.
if ( ! menuId ) {
setMenuId( menus[ 0 ].id );
@@ -57,7 +94,7 @@ export default function MenusEditor( { blockEditorSettings } ) {
return
;
}
- const hasMenus = !! stateMenus?.length;
+ const hasMenus = !! menus?.length;
const isCreateMenuPanelVisible =
hasCompletedFirstLoad && ( ! hasMenus || showCreateMenuPanel );
@@ -75,12 +112,12 @@ export default function MenusEditor( { blockEditorSettings } ) {
( {
+ options={ menus?.map( ( menu ) => ( {
value: menu.id,
label: menu.name,
} ) ) }
onChange={ ( selectedMenuId ) =>
- setMenuId( selectedMenuId )
+ setMenuId( Number( selectedMenuId ) )
}
value={ menuId }
/>
@@ -96,7 +133,7 @@ export default function MenusEditor( { blockEditorSettings } ) {
{ isCreateMenuPanelVisible && (
) }
{ hasMenus && (
- {
- const newStateMenus = stateMenus.filter( ( menu ) => {
- return menu.id !== deletedMenu;
+ onDeleteMenu={ async () => {
+ removeNotice( noticeId.current );
+ const deletedMenu = await deleteMenu( menuId, {
+ force: 'true',
} );
- setStateMenus( newStateMenus );
- if ( newStateMenus.length ) {
- setMenuId( newStateMenus[ 0 ].id );
- } else {
- setMenuId();
+ if ( deletedMenu ) {
+ setMenuId( false );
}
} }
/>
diff --git a/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js b/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js
new file mode 100644
index 0000000000000..2bb3fe8569629
--- /dev/null
+++ b/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js
@@ -0,0 +1,163 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ BlockList,
+ BlockToolbar,
+ NavigableToolbar,
+ ObserveTyping,
+ WritingFlow,
+ BlockInspector,
+} from '@wordpress/block-editor';
+import { useEffect, useState } from '@wordpress/element';
+import {
+ Button,
+ Card,
+ CardHeader,
+ CardBody,
+ CardFooter,
+ CheckboxControl,
+ Dropdown,
+ Popover,
+} from '@wordpress/components';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { cog } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import DeleteMenuButton from '../delete-menu-button';
+
+export default function BlockEditorArea( {
+ onDeleteMenu,
+ menuId,
+ saveBlocks,
+} ) {
+ const {
+ rootBlockId,
+ isNavigationModeActive,
+ isRootBlockSelected,
+ hasSelectedBlock,
+ } = useSelect( ( select ) => {
+ const {
+ isNavigationMode,
+ getBlockSelectionStart,
+ getBlock,
+ getBlocks,
+ } = select( 'core/block-editor' );
+
+ const selectionStartClientId = getBlockSelectionStart();
+ const rootClientId = getBlocks()[ 0 ]?.clientId;
+
+ return {
+ selectionStartClientId,
+ rootBlockId: rootClientId,
+ isNavigationModeActive: isNavigationMode(),
+ isRootBlockSelected:
+ !! selectionStartClientId &&
+ rootClientId === selectionStartClientId,
+ hasSelectedBlock:
+ !! selectionStartClientId &&
+ !! getBlock( selectionStartClientId ),
+ };
+ }, [] );
+
+ const { saveMenu } = useDispatch( 'core' );
+ const menu = useSelect( ( select ) => select( 'core' ).getMenu( menuId ), [
+ menuId,
+ ] );
+
+ const [ autoAddPages, setAutoAddPages ] = useState( false );
+
+ useEffect( () => {
+ if ( menu ) {
+ setAutoAddPages( menu.auto_add );
+ }
+ }, [ menuId ] );
+
+ // Select the navigation block when it becomes available
+ const { selectBlock } = useDispatch( 'core/block-editor' );
+ useEffect( () => {
+ if ( rootBlockId ) {
+ selectBlock( rootBlockId );
+ }
+ }, [ rootBlockId ] );
+
+ return (
+
+
+
+ { __( 'Navigation menu' ) }
+
+
+
+ (
+
+ ) }
+ renderContent={ () => (
+
+ ) }
+ />
+
+
+
+ { hasSelectedBlock && ! isRootBlockSelected && (
+
+ ) }
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setAutoAddPages( ! autoAddPages );
+ saveMenu( {
+ ...menu,
+ auto_add: ! autoAddPages,
+ } );
+ } }
+ checked={ autoAddPages }
+ />
+
+
+
+ );
+}
diff --git a/packages/edit-navigation/src/components/menu-editor/helpers.js b/packages/edit-navigation/src/components/navigation-editor/helpers.js
similarity index 100%
rename from packages/edit-navigation/src/components/menu-editor/helpers.js
rename to packages/edit-navigation/src/components/navigation-editor/helpers.js
diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js
new file mode 100644
index 0000000000000..6602f2f5913ca
--- /dev/null
+++ b/packages/edit-navigation/src/components/navigation-editor/index.js
@@ -0,0 +1,81 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ BlockEditorKeyboardShortcuts,
+ BlockEditorProvider,
+} from '@wordpress/block-editor';
+import { useViewportMatch } from '@wordpress/compose';
+import { Spinner } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import NavigationEditorShortcuts from './shortcuts';
+import BlockEditorArea from './block-editor-area';
+import NavigationStructureArea from './navigation-structure-area';
+import useNavigationBlockEditor from './use-navigation-block-editor';
+import { useDispatch, useSelect } from '@wordpress/data';
+
+export default function NavigationEditor( {
+ menuId,
+ blockEditorSettings,
+ onDeleteMenu,
+} ) {
+ const { post, hasResolved } = useSelect( ( select ) => ( {
+ post: select( 'core/edit-navigation' ).getNavigationPostForMenu(
+ menuId
+ ),
+ hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost(
+ menuId
+ ),
+ } ) );
+ return (
+
+
+
+
+ { ! hasResolved ? (
+
+ ) : (
+
+ ) }
+
+ );
+}
+
+function NavigationPostEditor( { post, blockEditorSettings, onDeleteMenu } ) {
+ const isLargeViewport = useViewportMatch( 'medium' );
+ const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post );
+ const { saveNavigationPost } = useDispatch( 'core/edit-navigation' );
+ const save = () => saveNavigationPost( post );
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/packages/edit-navigation/src/components/menu-editor/navigation-structure-area.js b/packages/edit-navigation/src/components/navigation-editor/navigation-structure-area.js
similarity index 62%
rename from packages/edit-navigation/src/components/menu-editor/navigation-structure-area.js
rename to packages/edit-navigation/src/components/navigation-editor/navigation-structure-area.js
index d1a831d71fd18..7c01de144d735 100644
--- a/packages/edit-navigation/src/components/menu-editor/navigation-structure-area.js
+++ b/packages/edit-navigation/src/components/navigation-editor/navigation-structure-area.js
@@ -10,23 +10,23 @@ import {
Panel,
PanelBody,
} from '@wordpress/components';
-import { useDispatch, useSelect } from '@wordpress/data';
+import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
export default function NavigationStructureArea( { blocks, initialOpen } ) {
- const isSmallScreen = useViewportMatch( 'medium', '<' );
- const selectedBlockClientIds = useSelect(
- ( select ) => select( 'core/block-editor' ).getSelectedBlockClientIds(),
- []
+ const [ selectedBlockId, setSelectedBlockId ] = useState(
+ blocks[ 0 ]?.clientId
);
- const { selectBlock } = useDispatch( 'core/block-editor' );
+ const isSmallScreen = useViewportMatch( 'medium', '<' );
const showNavigationStructure = !! blocks.length;
const content = showNavigationStructure && (
<__experimentalBlockNavigationTree
blocks={ blocks }
- selectedBlockClientId={ selectedBlockClientIds[ 0 ] }
- selectBlock={ selectBlock }
+ selectedBlockClientId={ selectedBlockId }
+ selectBlock={ ( id ) => {
+ setSelectedBlockId( id );
+ } }
__experimentalFeatures
showNestedBlocks
showAppender
@@ -35,7 +35,7 @@ export default function NavigationStructureArea( { blocks, initialOpen } ) {
);
return isSmallScreen ? (
-
+
) : (
-
-
+
+
{ __( 'Navigation structure' ) }
{ content }
diff --git a/packages/edit-navigation/src/components/navigation-editor/shortcuts.js b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js
new file mode 100644
index 0000000000000..d75d3f176e2e4
--- /dev/null
+++ b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js
@@ -0,0 +1,80 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useCallback } from '@wordpress/element';
+import { useDispatch } from '@wordpress/data';
+import { useShortcut } from '@wordpress/keyboard-shortcuts';
+import { __ } from '@wordpress/i18n';
+
+function NavigationEditorShortcuts( { saveBlocks } ) {
+ useShortcut(
+ 'core/edit-navigation/save-menu',
+ useCallback( ( event ) => {
+ event.preventDefault();
+ saveBlocks();
+ } ),
+ {
+ bindGlobal: true,
+ }
+ );
+
+ const { redo, undo } = useDispatch( 'core' );
+ useShortcut(
+ 'core/edit-navigation/undo',
+ ( event ) => {
+ undo();
+ event.preventDefault();
+ },
+ { bindGlobal: true }
+ );
+
+ useShortcut(
+ 'core/edit-navigation/redo',
+ ( event ) => {
+ redo();
+ event.preventDefault();
+ },
+ { bindGlobal: true }
+ );
+
+ return null;
+}
+
+function RegisterNavigationEditorShortcuts() {
+ const { registerShortcut } = useDispatch( 'core/keyboard-shortcuts' );
+ useEffect( () => {
+ registerShortcut( {
+ name: 'core/edit-navigation/save-menu',
+ category: 'global',
+ description: __( 'Save the navigation currently being edited.' ),
+ keyCombination: {
+ modifier: 'primary',
+ character: 's',
+ },
+ } );
+ registerShortcut( {
+ name: 'core/edit-navigation/undo',
+ category: 'global',
+ description: __( 'Undo your last changes.' ),
+ keyCombination: {
+ modifier: 'primary',
+ character: 'z',
+ },
+ } );
+ registerShortcut( {
+ name: 'core/edit-navigation/redo',
+ category: 'global',
+ description: __( 'Redo your last undo.' ),
+ keyCombination: {
+ modifier: 'primaryShift',
+ character: 'z',
+ },
+ } );
+ }, [ registerShortcut ] );
+
+ return null;
+}
+
+NavigationEditorShortcuts.Register = RegisterNavigationEditorShortcuts;
+
+export default NavigationEditorShortcuts;
diff --git a/packages/edit-navigation/src/components/menu-editor/style.scss b/packages/edit-navigation/src/components/navigation-editor/style.scss
similarity index 75%
rename from packages/edit-navigation/src/components/menu-editor/style.scss
rename to packages/edit-navigation/src/components/navigation-editor/style.scss
index 59ce953612352..26a20bfce1680 100644
--- a/packages/edit-navigation/src/components/menu-editor/style.scss
+++ b/packages/edit-navigation/src/components/navigation-editor/style.scss
@@ -1,4 +1,4 @@
-.edit-navigation-menu-editor {
+.edit-navigation-editor {
display: grid;
align-items: self-start;
grid-gap: 10px;
@@ -13,7 +13,7 @@
}
}
-.edit-navigation-menu-editor__block-editor-toolbar {
+.edit-navigation-editor__block-editor-toolbar {
height: 46px;
margin-bottom: 12px;
border: 1px solid #e2e4e7;
@@ -34,7 +34,7 @@
border: 0;
// Add a border after item groups to show as separator in the block toolbar.
- border-right: $border-width solid $light-gray-500;
+ border-right: $border-width solid $gray-200;
}
@@ -56,9 +56,16 @@
> :first-child button {
padding-left: 0;
}
+
+ &.is-selected .block-editor-block-icon svg,
+ &.is-selected:focus .block-editor-block-icon svg {
+ color: $dark-gray-600;
+ background: transparent;
+ box-shadow: none;
+ }
}
-.edit-navigation-menu-editor__navigation-structure-panel {
+.edit-navigation-editor__navigation-structure-panel {
// IE11 requires the column to be explicitly declared.
grid-column: 1;
@@ -73,11 +80,11 @@
}
}
-.edit-navigation-menu-editor__navigation-structure-header {
+.edit-navigation-editor__navigation-structure-header {
font-weight: bold;
}
-.edit-navigation-menu-editor__block-editor-area {
+.edit-navigation-editor__block-editor-area {
@include break-medium {
// IE11 requires the column to be explicitly declared.
// Only shift this into the second column on desktop.
@@ -91,7 +98,7 @@
justify-content: space-between;
}
- .edit-navigation-menu-editor__block-editor-area-header-text {
+ .edit-navigation-editor__block-editor-area-header-text {
flex-grow: 1;
font-weight: bold;
}
@@ -108,5 +115,16 @@
padding-top: $grid-unit-20;
width: 100%;
text-align: left;
- border-top: 1px solid $light-gray-300;
+ border-top: 1px solid $gray-200;
+}
+
+.edit-navigation-editor__block-inspector {
+ .components-button {
+ margin-left: $grid-unit-20;
+
+ &[aria-expanded="true"] {
+ color: $white;
+ background-color: $gray-900;
+ }
+ }
}
diff --git a/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js b/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js
new file mode 100644
index 0000000000000..57968c10b78d4
--- /dev/null
+++ b/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch } from '@wordpress/data';
+import { useCallback } from '@wordpress/element';
+import { useEntityBlockEditor } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { KIND, POST_TYPE } from '../../store/utils';
+
+export default function useNavigationBlockEditor( post ) {
+ const { createMissingMenuItems } = useDispatch( 'core/edit-navigation' );
+
+ const [ blocks, onInput, _onChange ] = useEntityBlockEditor(
+ KIND,
+ POST_TYPE,
+ { id: post.id }
+ );
+ const onChange = useCallback(
+ async ( updatedBlocks ) => {
+ await _onChange( updatedBlocks );
+ createMissingMenuItems( post );
+ },
+ [ blocks, _onChange ]
+ );
+
+ return [ blocks, onInput, onChange ];
+}
diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js
index 558b9949a0b3e..63e47f7d1589f 100644
--- a/packages/edit-navigation/src/index.js
+++ b/packages/edit-navigation/src/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { map } from 'lodash';
+import { map, set, flatten, partialRight } from 'lodash';
/**
* WordPress dependencies
@@ -16,48 +16,116 @@ import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
+import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import Layout from './components/layout';
+import './store';
+
+function disableInsertingNonNavigationBlocks( settings, name ) {
+ if ( ! [ 'core/navigation', 'core/navigation-link' ].includes( name ) ) {
+ set( settings, [ 'supports', 'inserter' ], false );
+ }
+ return settings;
+}
/**
* Fetches link suggestions from the API. This function is an exact copy of a function found at:
*
- * wordpress/editor/src/components/provider/index.js
+ * packages/editor/src/components/provider/index.js
*
* It seems like there is no suitable package to import this from. Ideally it would be either part of core-data.
* Until we refactor it, just copying the code is the simplest solution.
*
- * @param {Object} search
- * @param {number} perPage
+ * @param {string} search
+ * @param {Object} [searchArguments]
+ * @param {number} [searchArguments.isInitialSuggestions]
+ * @param {number} [searchArguments.type]
+ * @param {number} [searchArguments.subtype]
+ * @param {Object} [editorSettings]
+ * @param {boolean} [editorSettings.disablePostFormats=false]
* @return {Promise