diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ccb59aa2df443..e9a47c7322e476 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -166,9 +166,9 @@ Maintaining dozens of npm packages is difficult—it can be tough to keep track The developer who proposes a change (pull request) is responsible to choose the correct version increment (`major`, `minor`, or `patch`) according to the following guidelines: -- Major version X (X.y.z | X > 0) should be changed with any backwards-incompatible/"breaking" change. This will usually occur at the final stage of deprecating and removing of a feature. -- Minor version Y (x.Y.z | x > 0) should be changed when you add functionality or change functionality in a backwards-compatible manner. It must be incremented if any public API functionality is marked as deprecated. -- Patch version Z (x.y.Z | x > 0) should be incremented when you make backwards-compatible bug fixes. +- Major version X (X.y.z | X > 0) should be changed with any backward incompatible/"breaking" change. This will usually occur at the final stage of deprecating and removing of a feature. +- Minor version Y (x.Y.z | x > 0) should be changed when you add functionality or change functionality in a backward compatible manner. It must be incremented if any public API functionality is marked as deprecated. +- Patch version Z (x.y.Z | x > 0) should be incremented when you make backward compatible bug fixes. When in doubt, refer to [Semantic Versioning specification](https://semver.org/). diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8f7b5d83932b64..8279df075b66bb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -122,3 +122,5 @@ This list is manually curated to include valuable contributions by volunteers th | @sharazghouri | @sharaz | | @jakeparis | @jakeparis | | @designsimply | @designsimply | +| @aldavigdis | @aldavigdis | +| @miya0001 | @miyauchi | diff --git a/README.md b/README.md index 03251ceba2160e..855958ef9635ef 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repo is the development hub for the editor focus in WordPress Core. `Gutenberg` is the project name. ## Getting started -- **Download:** If you want to use the latest release with your WordPress site, download the latest release from the WordPress.org plugins repository. +- **Download:** If you want to use the latest release with your WordPress site, download the latest release from the WordPress.org plugins repository. - **Discuss:** Conversations and discussions take place in `#core-editor` channel on the Making WordPress Slack. - **Contribute:** Development of Gutenberg happens in this GitHub repo. Get started by reading the contributing guidelines. - **Learn:** Discover more about the project on WordPress.org. @@ -44,7 +44,7 @@ Check out the `) to be output incorrectly. This will cause the browser to render using "Quirks Mode", which is a compatibility layer that gets enabled when the browser doesn't know what type of document it is parsing. The block editor is not meant to work in this mode, but it can _appear_ to be working just fine. If you encounter issues such as *meta boxes overlaying the editor* or other layout issues, please check the raw page source of your document to see that the document type definition is the first thing output on the page. There will also be a warning in the JavaScript console, noting the issue. diff --git a/docs/designers-developers/developers/backwards-compatibility/README.md b/docs/designers-developers/developers/backwards-compatibility/README.md deleted file mode 100644 index bd453cba0e56a4..00000000000000 --- a/docs/designers-developers/developers/backwards-compatibility/README.md +++ /dev/null @@ -1 +0,0 @@ -# Backwards Compatibility diff --git a/docs/designers-developers/developers/block-api/README.md b/docs/designers-developers/developers/block-api/README.md index 1301449ecdf2c1..2c18a261d8fc5c 100644 --- a/docs/designers-developers/developers/block-api/README.md +++ b/docs/designers-developers/developers/block-api/README.md @@ -4,8 +4,8 @@ Blocks are the fundamental element of the Gutenberg editor. They are the primary ## Registering a block -All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](block-api/block-registration.md) documentation. +All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](../../../../docs/designers-developers/developers/block-api/block-registration.md) documentation. ## Block `edit` and `save` -The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](block-api/block-edit-save.md). +The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](../../../../docs/designers-developers/developers/block-api/block-edit-save.md). diff --git a/docs/designers-developers/developers/themes/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md index abc611dc5d000b..a5da2c7199e97b 100644 --- a/docs/designers-developers/developers/themes/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -4,7 +4,7 @@ The new Blocks include baseline support in all themes, enhancements to opt-in to There are a few new concepts to consider when building themes: -- **Editor Color Palette** - A default set of colors is provided, but themes and register their own and optionally lock users into picking from the defined palette. +- **Editor Color Palette** - A default set of colors is provided, but themes can register their own and optionally lock users into picking from the defined palette. - **Editor Text Size Palette** - A default set of sizes is provided, but themes and register their own and optionally lock users into picking from preselected sizes. - **Responsive Embeds** - Themes must opt-in to responsive embeds. - **Frontend & Editor Styles** - To get the most out of blocks, theme authors will want to make sure Core styles look good and opt-in, or write their own styles to best fit their theme. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md index de6509e4c42b73..1bad64b3d95329 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md @@ -4,7 +4,7 @@ To simplify block customization and ensure a consistent experience for users, th ## Toolbar -toolbar +![Screenshot of the rich text toolbar applied to a paragraph block inside the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/toolbar-text.png) When the user selects a block, a number of control buttons may be shown in a toolbar above the selected block. Some of these block-level controls are included automatically if the editor is able to transform the block to another type, or if the focused element is an RichText component. @@ -171,7 +171,7 @@ Note that `BlockControls` is only visible when the block is currently selected a ## Inspector -inspector +![Screenshot of the inspector panel focused on the settings for a paragraph block](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/inspector.png) The inspector is used to display less-often-used settings or settings that require more screen space. The inspector should be used for **block-level settings only**. diff --git a/docs/designers-developers/glossary.md b/docs/designers-developers/glossary.md index afa574179db276..f51c0d16d5dbab 100644 --- a/docs/designers-developers/glossary.md +++ b/docs/designers-developers/glossary.md @@ -1,21 +1,61 @@ # Glossary -- **Attribute sources**: An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function. -- **Attributes**: The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block. -- **Block**: The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. -- **Block Categories**: These are not a WordPress taxonomy, but instead used internally to sort blocks in the Block Inserter. -- **Block Inserter**: Primary interface for selecting from the available blocks, triggered by plus icon buttons on Blocks or in the top-left of the editor interface. -- **Block name**: A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. `core/image` -- **Block type**: In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition. -- **Classic block**: -- **Dynamic block**: A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering. -- **RichText**: A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. -- **Inspector**: A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block. -- **Post settings**: A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image. -- **Reusable block**: -- **Sidebar**: -- **Serialization**: The process of converting a block's attributes object into HTML markup, typically occurring when saving the post. -- **Static block**: A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content. -- **TinyMCE**: [TinyMCE](https://www.tinymce.com/) is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor. -- **Toolbar**: A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block. -- **Template**: +
+
Attribute sources
+
An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function.
+ +
Attributes
+
The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block.
+ +
Block
+
The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience.
+ +
Block Categories
+
These are not a WordPress taxonomy, but instead used internally to sort blocks in the Block Inserter.
+ +
Block Inserter
+
Primary interface for selecting from the available blocks, triggered by plus icon buttons on Blocks or in the top-left of the editor interface.
+ +
Block name
+
A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. core/image
+ +
Block type
+
In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition.
+ +
Classic block
+
A block which embeds the TinyMCE editor as a block, TinyMCE was the base of the previous core editor. Older content created prior to the block editor will be loaded in to a single Classic block.
+ +
Dynamic block
+
A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering.
+ +
Inspector
+
A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block.
+ +
Post settings
+
A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image.
+ +
RichText
+
A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library.
+ +
Reusable block
+
A block that is saved and then can be shared as a reusable, repeatable piece of content.
+ +
Sidebar
+
The panel on the right which contains the document and block settings. The sidebar is toggled using the Settings gear icon.
+ +
Serialization
+
The process of converting a block's attributes object into HTML markup, which occurs each time a block is edited.
+ +
Static block
+
A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content.
+ +
TinyMCE
+
TinyMCE is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor.
+ +
Toolbar
+
A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block.
+ +
Template
+
A template is a pre-defined arrangement of blocks, possibly with predefined attributes or placeholder content. You can provide a template for a post type, to give users a starting point when creating a new piece of content, or inside a custom block with the InnerBlocks component. See the templates documentation for more information. See templates documentation for more information.
+ +
diff --git a/docs/manifest.json b/docs/manifest.json index 2dae91884153aa..e8925eec9fb9ce 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -132,22 +132,22 @@ "parent": "themes" }, { - "title": "Backwards Compatibility", - "slug": "backwards-compatibility", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/README.md", + "title": "Backward Compatibility", + "slug": "backward-compatibility", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/README.md", "parent": "developers" }, { "title": "Deprecations", "slug": "deprecations", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/deprecations.md", - "parent": "backwards-compatibility" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/deprecations.md", + "parent": "backward-compatibility" }, { "title": "Meta Boxes", "slug": "meta-box", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/meta-box.md", - "parent": "backwards-compatibility" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/meta-box.md", + "parent": "backward-compatibility" }, { "title": "Tutorials", diff --git a/docs/toc.json b/docs/toc.json index 211c74631ec700..6dd5e876cbc00c 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -24,9 +24,9 @@ {"docs/designers-developers/developers/themes/README.md": [ {"docs/designers-developers/developers/themes/theme-support.md": []} ]}, - {"docs/designers-developers/developers/backwards-compatibility/README.md": [ - {"docs/designers-developers/developers/backwards-compatibility/deprecations.md": []}, - {"docs/designers-developers/developers/backwards-compatibility/meta-box.md": []} + {"docs/designers-developers/developers/backward-compatibility/README.md": [ + {"docs/designers-developers/developers/backward-compatibility/deprecations.md": []}, + {"docs/designers-developers/developers/backward-compatibility/meta-box.md": []} ]}, {"docs/designers-developers/developers/tutorials/readme.md": [ {"docs/designers-developers/developers/tutorials/block-tutorial/readme.md" :[ diff --git a/gutenberg.php b/gutenberg.php index 2f75a595903e79..9f9d7b183aeb55 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 4.6.1 + * Version: 4.7.0-rc.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/lib/client-assets.php b/lib/client-assets.php index 6fa221bf4c5d8c..c1a6e847bba5e9 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -391,7 +391,7 @@ function gutenberg_register_scripts_and_styles() { wp_add_inline_script( 'wp-block-library', $script, 'before' ); // Editor Styles. - // This empty stylesheet is defined to ensure backwards compatibility. + // This empty stylesheet is defined to ensure backward compatibility. gutenberg_override_style( 'wp-blocks', false ); $fonts_url = ''; @@ -461,6 +461,7 @@ function gutenberg_register_scripts_and_styles() { array( 'wp-components', 'wp-editor', + 'wp-block-library', // Always include visual styles so the editor never appears broken. 'wp-block-library-theme', ), diff --git a/lib/load.php b/lib/load.php index ee1c973f99568a..0c55db3a37f6ca 100644 --- a/lib/load.php +++ b/lib/load.php @@ -62,10 +62,7 @@ if ( ! function_exists( 'render_block_core_categories' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/categories/index.php'; } -// Currently merged in core as `gutenberg_render_block_core_latest_comments`, -// expected to change soon. -if ( ! function_exists( 'render_block_core_latest_comments' ) - && ! function_exists( 'gutenberg_render_block_core_latest_comments' ) ) { +if ( ! function_exists( 'render_block_core_latest_comments' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/latest-comments/index.php'; } if ( ! function_exists( 'render_block_core_latest_posts' ) ) { diff --git a/package-lock.json b/package-lock.json index df6980879a3426..347cbcd5337484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.1", + "version": "4.7.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15030,9 +15030,9 @@ } }, "node-sass": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", - "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -15048,14 +15048,26 @@ "meow": "^3.7.0", "mkdirp": "^0.5.1", "nan": "^2.10.0", - "node-gyp": "^3.3.1", + "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "2.87.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, "dependencies": { + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -15068,6 +15080,12 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", @@ -15076,7 +15094,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -15086,7 +15104,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -15107,6 +15125,28 @@ "which": "^1.2.9" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -15116,15 +15156,11 @@ "repeating": "^2.0.0" } }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "map-obj": { "version": "1.0.1", @@ -15134,7 +15170,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -15150,12 +15186,33 @@ "trim-newlines": "^1.0.0" } }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "~1.37.0" + } + }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -15166,9 +15223,37 @@ "strip-indent": "^1.0.1" } }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -18883,7 +18968,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -18903,7 +18988,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -19038,7 +19123,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { @@ -19674,9 +19759,9 @@ "dev": true }, "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "dev": true, "requires": { "readable-stream": "^2.0.1" @@ -20723,27 +20808,12 @@ "dev": true }, "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "dev": true, "requires": { - "glob": "^6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "glob": "^7.1.2" } }, "tryer": { diff --git a/package.json b/package.json index 541ebbfa9df50d..432662628f9e2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.1", + "version": "4.7.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", @@ -90,7 +90,7 @@ "lint-staged": "7.2.0", "lodash": "4.17.10", "mkdirp": "0.5.1", - "node-sass": "4.9.2", + "node-sass": "4.11.0", "path-type": "3.0.0", "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js index 5095fc473d67e6..e2677bbf6a9b3a 100644 --- a/packages/annotations/src/block/index.js +++ b/packages/annotations/src/block/index.js @@ -17,7 +17,7 @@ const addAnnotationClassName = ( OriginalComponent ) => { return { className: annotations.map( ( annotation ) => { return 'is-annotated-by-' + annotation.source; - } ), + } ).join( ' ' ), }; } )( OriginalComponent ); }; diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 2502c0f7e0eb5f..f0eb37ff30ca9b 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -78,6 +78,7 @@ export default class ClassicEdit extends Component { onSetup( editor ) { const { attributes: { content }, setAttributes } = this.props; const { ref } = this; + let bookmark; this.editor = editor; @@ -86,12 +87,25 @@ export default class ClassicEdit extends Component { } editor.on( 'blur', () => { + bookmark = editor.selection.getBookmark( 2, true ); + setAttributes( { content: editor.getContent(), } ); + + editor.once( 'focus', () => { + if ( bookmark ) { + editor.selection.moveToBookmark( bookmark ); + } + } ); + return false; } ); + editor.on( 'mousedown touchstart', () => { + bookmark = null; + } ); + editor.on( 'keydown', ( event ) => { if ( ( event.keyCode === BACKSPACE || event.keyCode === DELETE ) && isTmceEmpty( editor ) ) { // delete the block diff --git a/packages/block-library/src/embed/editor.scss b/packages/block-library/src/embed/editor.scss index 0b79d5b0414beb..29cbd9fb932cff 100644 --- a/packages/block-library/src/embed/editor.scss +++ b/packages/block-library/src/embed/editor.scss @@ -25,4 +25,9 @@ font-size: $default-font-size; } } + + // Stops long URLs from breaking out of the no preview available screen + .components-placeholder__error { + word-break: break-word; + } } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index cb059b5bfcdc60..f7fdd73a9aaa54 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -121,7 +121,7 @@ class ImageEdit extends Component { } componentDidMount() { - const { attributes, setAttributes } = this.props; + const { attributes, setAttributes, noticeOperations } = this.props; const { id, url = '' } = attributes; if ( isTemporaryImage( id, url ) ) { @@ -134,6 +134,10 @@ class ImageEdit extends Component { setAttributes( pickRelevantMediaFiles( image ) ); }, allowedTypes: ALLOWED_MEDIA_TYPES, + onError: ( message ) => { + noticeOperations.createErrorNotice( message ); + this.setState( { isEditing: true } ); + }, } ); } } @@ -414,7 +418,7 @@ class ImageEdit extends Component { ); - if ( isEditing ) { + if ( isEditing || ! url ) { const src = isExternal ? url : undefined; return ( diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index 29e17e9de50080..fcc17c3cd6a004 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -5,34 +5,32 @@ * @package WordPress */ -if ( ! function_exists( 'gutenberg_draft_or_post_title' ) ) { - /** - * Get the post title. - * - * The post title is fetched and if it is blank then a default string is - * returned. - * - * Copied from `wp-admin/includes/template.php`, but we can't include that - * file because: - * - * 1. It causes bugs with test fixture generation and strange Docker 255 error - * codes. - * 2. It's in the admin; ideally we *shouldn't* be including files from the - * admin for a block's output. It's a very small/simple function as well, - * so duplicating it isn't too terrible. - * - * @since 3.3.0 - * - * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. - * @return string The post title if set; "(no title)" if no title is set. - */ - function gutenberg_draft_or_post_title( $post = 0 ) { - $title = get_the_title( $post ); - if ( empty( $title ) ) { - $title = __( '(no title)' ); - } - return esc_html( $title ); +/** + * Get the post title. + * + * The post title is fetched and if it is blank then a default string is + * returned. + * + * Copied from `wp-admin/includes/template.php`, but we can't include that + * file because: + * + * 1. It causes bugs with test fixture generation and strange Docker 255 error + * codes. + * 2. It's in the admin; ideally we *shouldn't* be including files from the + * admin for a block's output. It's a very small/simple function as well, + * so duplicating it isn't too terrible. + * + * @since 3.3.0 + * + * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. + * @return string The post title if set; "(no title)" if no title is set. + */ +function wp_latest_comments_draft_or_post_title( $post = 0 ) { + $title = get_the_title( $post ); + if ( empty( $title ) ) { + $title = __( '(no title)' ); } + return esc_html( $title ); } /** @@ -42,7 +40,7 @@ function gutenberg_draft_or_post_title( $post = 0 ) { * * @return string Returns the post content with latest comments added. */ -function gutenberg_render_block_core_latest_comments( $attributes = array() ) { +function render_block_core_latest_comments( $attributes = array() ) { // This filter is documented in wp-includes/widgets/class-wp-widget-recent-comments.php. $comments = get_comments( apply_filters( @@ -94,7 +92,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { // `_draft_or_post_title` calls `esc_html()` so we don't need to wrap that call in // `esc_html`. - $post_title = '' . gutenberg_draft_or_post_title( $comment->comment_post_ID ) . ''; + $post_title = '' . wp_latest_comments_draft_or_post_title( $comment->comment_post_ID ) . ''; $list_items_markup .= sprintf( /* translators: 1: author name (inside or tag, based on if they have a URL), 2: post title related to this comment */ @@ -179,6 +177,6 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { 'enum' => array( 'center', 'left', 'right', 'wide', 'full', '' ), ), ), - 'render_callback' => 'gutenberg_render_block_core_latest_comments', + 'render_callback' => 'render_block_core_latest_comments', ) ); diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index c1cabf248e57ca..9f9b626f837fd2 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -7,7 +7,7 @@ import { get } from 'lodash'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { BlockControls, InnerBlocks, @@ -32,7 +32,7 @@ import MediaContainer from './media-container'; */ const ALLOWED_BLOCKS = [ 'core/button', 'core/paragraph', 'core/heading', 'core/list' ]; const TEMPLATE = [ - [ 'core/paragraph', { fontSize: 'large', placeholder: 'Content…' } ], + [ 'core/paragraph', { fontSize: 'large', placeholder: _x( 'Content…', 'content placeholder' ) } ], ]; class MediaTextEdit extends Component { diff --git a/packages/block-library/src/paragraph/editor.scss b/packages/block-library/src/paragraph/editor.scss index 98ea5e4e2b85a4..1110e5d5832d4d 100644 --- a/packages/block-library/src/paragraph/editor.scss +++ b/packages/block-library/src/paragraph/editor.scss @@ -1,6 +1,11 @@ // Specific to the empty paragraph placeholder: -// when shown on mobile and in nested contexts, the plus to add blocks shows up on the right. +// when shown on mobile and in nested contexts, one or more icons show up on the right. // This padding makes sure it doesn't overlap text. .editor-rich-text__tinymce[data-is-placeholder-visible="true"] + .editor-rich-text__tinymce.wp-block-paragraph { - padding-right: $icon-button-size; + padding-right: $icon-button-size * 3; + + // In nested contexts only one icon shows up. + .wp-block .wp-block & { + padding-right: $icon-button-size; + } } diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index 0bfeb8b0fe2586..ea3fe579084410 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -67,5 +67,34 @@ describe( 'block helpers', () => { expect( isUnmodifiedDefaultBlock( block ) ).toBe( false ); } ); + + it( 'should invalidate cache if the default block name changed', () => { + registerBlockType( 'core/test-block1', { + attributes: { + includesDefault1: { + type: 'boolean', + default: true, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + registerBlockType( 'core/test-block2', { + attributes: { + includesDefault2: { + type: 'boolean', + default: true, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + setDefaultBlockName( 'core/test-block1' ); + isUnmodifiedDefaultBlock( createBlock( 'core/test-block1' ) ); + setDefaultBlockName( 'core/test-block2' ); + expect( isUnmodifiedDefaultBlock( createBlock( 'core/test-block2' ) ) ).toBe( true ); + } ); } ); } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 392593b440096e..6dcabbcbcea012 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -38,7 +38,16 @@ export function isUnmodifiedDefaultBlock( block ) { return false; } - const newDefaultBlock = createBlock( defaultBlockName ); + // Cache a created default block if no cache exists or the default block + // name changed. + if ( + ! isUnmodifiedDefaultBlock.block || + isUnmodifiedDefaultBlock.block.name !== defaultBlockName + ) { + isUnmodifiedDefaultBlock.block = createBlock( defaultBlockName ); + } + + const newDefaultBlock = isUnmodifiedDefaultBlock.block; const blockType = getBlockType( defaultBlockName ); return every( blockType.attributes, ( value, key ) => diff --git a/packages/components/src/font-size-picker/index.js b/packages/components/src/font-size-picker/index.js index 16329316d5ad1a..2908b3879c1ebf 100644 --- a/packages/components/src/font-size-picker/index.js +++ b/packages/components/src/font-size-picker/index.js @@ -6,8 +6,7 @@ import { map } from 'lodash'; /** * WordPress dependencies */ -import { __, _x } from '@wordpress/i18n'; -import { withInstanceId } from '@wordpress/compose'; +import { __, _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -37,6 +36,7 @@ function FontSizePicker( { }; const currentFont = fontSizes.find( ( font ) => font.size === value ); + const currentFontSizeName = ( currentFont && currentFont.name ) || ( ! value && _x( 'Normal', 'font size name' ) ) || _x( 'Custom', 'font size name' ); return ( @@ -51,26 +51,34 @@ function FontSizePicker( { isLarge onClick={ onToggle } aria-expanded={ isOpen } - aria-label={ __( 'Custom font size' ) } + aria-label={ sprintf( + /* translators: %s: font size name */ + __( 'Font size: %s' ), currentFontSizeName + ) } > - { ( currentFont && currentFont.name ) || ( ! value && _x( 'Normal', 'font size name' ) ) || _x( 'Custom', 'font size name' ) } + { currentFontSizeName } ) } renderContent={ () => ( - { map( fontSizes, ( { name, size, slug } ) => ( - - ) ) } + { map( fontSizes, ( { name, size, slug } ) => { + const isSelected = ( value === size || ( ! value && slug === 'normal' ) ); + + return ( + + ); + } ) } ) } /> @@ -90,7 +98,6 @@ function FontSizePicker( { onClick={ () => onChange( undefined ) } isSmall isDefault - aria-label={ __( 'Reset font size' ) } > { __( 'Reset' ) } @@ -112,4 +119,4 @@ function FontSizePicker( { ); } -export default withInstanceId( FontSizePicker ); +export default FontSizePicker; diff --git a/packages/components/src/server-side-render/README.md b/packages/components/src/server-side-render/README.md index cccde285c645df..ee18a7f4294ed6 100644 --- a/packages/components/src/server-side-render/README.md +++ b/packages/components/src/server-side-render/README.md @@ -2,7 +2,7 @@ ServerSideRender is a component used for server-side rendering a preview of dynamic blocks to display in the editor. Server-side rendering in a block's `edit` function should be limited to blocks that are heavily dependent on existing PHP rendering logic that is heavily intertwined with data, particularly when there are no endpoints available. -ServerSideRender may also be used when a legacy block is provided as a backwards compatibility measure, rather than needing to re-write the deprecated code that the block may depend on. +ServerSideRender may also be used when a legacy block is provided as a backward compatibility measure, rather than needing to re-write the deprecated code that the block may depend on. ServerSideRender should be regarded as a fallback or legacy mechanism, it is not appropriate for developing new features against. @@ -20,7 +20,7 @@ const MyServerSideRender = () => ( block="core/archives" attributes={ { showPostCounts: true, - displayAsDropdown: false, + displayAsDropdown: false, } } /> ); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 3fdf099954c982..e4b0c1ad32c680 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.1.5 (Unreleased) + +### Bug Fixes + - Fix saving WYSIWYG Meta Boxes + ## 3.1.4 (2018-11-30) ## 3.1.3 (2018-11-30) diff --git a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js index 31bc0becf225f2..970cf26355e6ca 100644 --- a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js +++ b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js @@ -17,9 +17,16 @@ class MetaBoxVisibility extends Component { updateDOM() { const { id, isVisible } = this.props; + const element = document.getElementById( id ); - if ( element ) { - element.style.display = isVisible ? '' : 'none'; + if ( ! element ) { + return; + } + + if ( isVisible ) { + element.classList.remove( 'is-hidden' ); + } else { + element.classList.add( 'is-hidden' ); } } diff --git a/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss b/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss index f336804fa38de1..b86d15952683b0 100644 --- a/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss +++ b/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss @@ -75,6 +75,12 @@ right: 20px; z-index: z-index(".edit-post-meta-boxes-area .spinner"); } + + // Hide disabled meta boxes using CSS so that we don't interfere with plugins + // that modify `element.style.display` on the meta box. + .is-hidden { + display: none; + } } .edit-post-meta-boxes-area__clear { diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index bce8285f72b0b4..59e870e7929da2 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -68,6 +68,13 @@ export function initializeEditor( id, postType, postId, settings, initialEdits ) registerCoreBlocks(); + // Show a console log warning if the browser is not in Standards rendering mode. + const documentMode = document.compatMode === 'CSS1Compat' ? 'Standards' : 'Quirks'; + if ( documentMode !== 'Standards' ) { + // eslint-disable-next-line no-console + console.warn( "Your browser is using Quirks Mode. \nThis can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening . Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins." ); + } + dispatch( 'core/nux' ).triggerGuide( [ 'core/editor.inserter', 'core/editor.settings', diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index 699b08f0636ed2..a177c36c1b761b 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -72,9 +72,14 @@ const effects = { } ); }, REQUEST_META_BOX_UPDATES( action, store ) { + // Saves the wp_editor fields + if ( window.tinyMCE ) { + window.tinyMCE.triggerSave(); + } + const state = store.getState(); - // Additional data needed for backwards compatibility. + // Additional data needed for backward compatibility. // If we do not provide this data, the post will be overridden with the default values. const post = select( 'core/editor' ).getCurrentPost( state ); const additionalData = [ diff --git a/packages/editor/src/components/default-block-appender/index.js b/packages/editor/src/components/default-block-appender/index.js index ecfe98d351a905..d4b95c4d961bae 100644 --- a/packages/editor/src/components/default-block-appender/index.js +++ b/packages/editor/src/components/default-block-appender/index.js @@ -7,7 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; +import { compose, withState } from '@wordpress/compose'; import { getDefaultBlockName } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -26,6 +26,8 @@ export function DefaultBlockAppender( { showPrompt, placeholder, rootClientId, + hovered, + setState, } ) { if ( isLocked || ! isVisible ) { return null; @@ -49,7 +51,12 @@ export function DefaultBlockAppender( { // The wp-block className is important for editor styles. return ( -
+
setState( { hovered: true } ) } + onMouseLeave={ () => setState( { hovered: false } ) } + > - + { hovered && }
); } export default compose( + withState( { hovered: false } ), withSelect( ( select, ownProps ) => { const { getBlockCount, getBlockName, isBlockValid, getEditorSettings, getTemplateLock } = select( 'core/editor' ); diff --git a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap index df9daeebd54f21..dc26d4405d8636 100644 --- a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap @@ -4,6 +4,8 @@ exports[`DefaultBlockAppender should append a default block when input focused 1
- @@ -33,6 +34,8 @@ exports[`DefaultBlockAppender should match snapshot 1`] = `
- @@ -55,6 +57,8 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = `
- diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 5c31e9e271de8f..b735ebcd5324ed 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -80,6 +80,7 @@ function writeInterstitialMessage( targetDocument ) { `; targetDocument.write( markup ); + targetDocument.title = __( 'Generating preview…' ); targetDocument.close(); } diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 872049c948e6a2..0731916d6a1a73 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -134,12 +134,9 @@ export class RichText extends Component { } /** - * Handles the onSetup event for the TinyMCE component. + * Sets a reference to the TinyMCE editor instance. * - * Will setup event handlers for the TinyMCE instance. - * An `onSetup` function in the props will be called if it is present. - * - * @param {tinymce} editor The editor instance as passed by TinyMCE. + * @param {Editor} editor The editor instance as passed by TinyMCE. */ onSetup( editor ) { this.editor = editor; @@ -691,9 +688,12 @@ export class RichText extends Component { if ( shouldReapply ) { const record = this.formatToValue( value ); - // Maintain the previous selection: - record.start = this.state.start; - record.end = this.state.end; + // Maintain the previous selection if the instance is currently + // selected. + if ( isSelected ) { + record.start = this.state.start; + record.end = this.state.end; + } this.applyRecord( record ); } diff --git a/packages/editor/src/components/word-count/index.js b/packages/editor/src/components/word-count/index.js index 176435cdc76ebb..719351ba323829 100644 --- a/packages/editor/src/components/word-count/index.js +++ b/packages/editor/src/components/word-count/index.js @@ -2,11 +2,19 @@ * WordPress dependencies */ import { withSelect } from '@wordpress/data'; +import { _x } from '@wordpress/i18n'; import { count as wordCount } from '@wordpress/wordcount'; function WordCount( { content } ) { + /* + * translators: If your word count is based on single characters (e.g. East Asian characters), + * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'. + * Do not translate into your own language. + */ + const wordCountType = _x( 'words', 'Word count type. Do not translate!' ); + return ( - { wordCount( content, 'words' ) } + { wordCount( content, wordCountType ) } ); } diff --git a/packages/editor/src/hooks/anchor.js b/packages/editor/src/hooks/anchor.js index 66b1c4f01eef4a..3af32e39ef2ac4 100644 --- a/packages/editor/src/hooks/anchor.js +++ b/packages/editor/src/hooks/anchor.js @@ -98,7 +98,7 @@ export const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => */ export function addSaveProps( extraProps, blockType, attributes ) { if ( hasBlockSupport( blockType, 'anchor' ) ) { - extraProps.id = attributes.anchor; + extraProps.id = attributes.anchor === '' ? null : attributes.anchor; } return extraProps; diff --git a/packages/editor/src/hooks/test/anchor.js b/packages/editor/src/hooks/test/anchor.js index 74bfa2e20dfc25..21a5c65be83cb0 100644 --- a/packages/editor/src/hooks/test/anchor.js +++ b/packages/editor/src/hooks/test/anchor.js @@ -62,5 +62,17 @@ describe( 'anchor', () => { expect( extraProps.id ).toBe( 'foo' ); } ); + + it( 'should remove an anchor attribute ID when feild is cleared', () => { + const attributes = { anchor: '' }; + const extraProps = getSaveContentExtraProps( {}, { + ...blockSettings, + supports: { + anchor: true, + }, + }, attributes ); + + expect( extraProps.id ).toBe( null ); + } ); } ); } ); diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 07a609d7278fa2..8d5c6c93c35eee 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,5 +1,6 @@ { "name": "@wordpress/eslint-config", + "private": true, "version": "1.0.0-alpha.0", "description": "ESLint config for WordPress development.", "author": "The WordPress Contributors", diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 1c34d8680dd2b3..c811bbc6180595 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -226,21 +226,17 @@ export function apply( { export function applyValue( future, current ) { let i = 0; + let futureChild; - while ( future.firstChild ) { + while ( ( futureChild = future.firstChild ) ) { const currentChild = current.childNodes[ i ]; - const futureNodeType = future.firstChild.nodeType; if ( ! currentChild ) { - current.appendChild( future.firstChild ); - } else if ( - futureNodeType !== currentChild.nodeType || - futureNodeType !== TEXT_NODE || - future.firstChild.nodeValue !== currentChild.nodeValue - ) { - current.replaceChild( future.firstChild, currentChild ); + current.appendChild( futureChild ); + } else if ( ! currentChild.isEqualNode( futureChild ) ) { + current.replaceChild( futureChild, currentChild ); } else { - future.removeChild( future.firstChild ); + future.removeChild( futureChild ); } i++; @@ -251,6 +247,25 @@ export function applyValue( future, current ) { } } +/** + * Returns true if two ranges are equal, or false otherwise. Ranges are + * considered equal if their start and end occur in the same container and + * offset. + * + * @param {Range} a First range object to test. + * @param {Range} b First range object to test. + * + * @return {boolean} Whether the two ranges are equal. + */ +function isRangeEqual( a, b ) { + return ( + a.startContainer === b.startContainer && + a.startOffset === b.startOffset && + a.endContainer === b.endContainer && + a.endOffset === b.endOffset + ); +} + export function applySelection( selection, current ) { const { node: startContainer, offset: startOffset } = getNodeByPath( current, selection.startPath ); const { node: endContainer, offset: endOffset } = getNodeByPath( current, selection.endPath ); @@ -283,6 +298,15 @@ export function applySelection( selection, current ) { range.setEnd( endContainer, endOffset ); } - windowSelection.removeAllRanges(); + if ( windowSelection.rangeCount > 0 ) { + // If the to be added range and the live range are the same, there's no + // need to remove the live range and add the equivalent range. + if ( isRangeEqual( range, windowSelection.getRangeAt( 0 ) ) ) { + return; + } + + windowSelection.removeAllRanges(); + } + windowSelection.addRange( range ); } diff --git a/packages/url/README.md b/packages/url/README.md index 4c092d670fc9fd..fb78d8808bb2be 100644 --- a/packages/url/README.md +++ b/packages/url/README.md @@ -167,4 +167,20 @@ const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=fooba Removes one or more query string arguments from the given URL. +### safeDecodeURI + +```js +const badUri = safeDecodeURI( '%z' ); // does not throw an Error, simply returns '%z' +``` + +Safely decodes a URI with `decodeURI`. Returns the URI unmodified if `decodeURI` throws an Error. + +### filterURLForDisplay + +```js +const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg +``` + +Returns a URL for display, without protocol, www subdomain, or trailing slash. +

Code is Poetry.

diff --git a/packages/viewport/src/store/selectors.js b/packages/viewport/src/store/selectors.js index ea8308d6ee156a..d379beeb5052fa 100644 --- a/packages/viewport/src/store/selectors.js +++ b/packages/viewport/src/store/selectors.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { takeRight } from 'lodash'; - /** * Returns true if the viewport matches the given query, or false otherwise. * @@ -20,9 +15,10 @@ import { takeRight } from 'lodash'; * @return {boolean} Whether viewport matches query. */ export function isViewportMatch( state, query ) { - // Pad to _at least_ two elements to take from the right, effectively - // defaulting the left-most value. - const key = takeRight( [ '>=', ...query.split( ' ' ) ], 2 ).join( ' ' ); + // Default to `>=` if no operator is present. + if ( query.indexOf( ' ' ) === -1 ) { + query = '>= ' + query; + } - return !! state[ key ]; + return !! state[ query ]; } diff --git a/phpunit/class-parsing-test.php b/phpunit/class-parsing-test.php deleted file mode 100644 index b854f8e306a2bd..00000000000000 --- a/phpunit/class-parsing-test.php +++ /dev/null @@ -1,105 +0,0 @@ -parse( _gutenberg_utf8_split( $html ) ); - - $this->assertEquals( - $expected_parsed, - $result, - "File '$parsed_json_filename' does not match expected value" - ); - } - - /** - * @dataProvider parsing_test_filenames - */ - function test_default_parser_output( $html_filename, $parsed_json_filename ) { - // include the parser if it was not yet loaded. - require_once dirname( __FILE__ ) . '/../packages/block-serialization-default-parser/parser.php'; - $html_path = self::$fixtures_dir . '/' . $html_filename; - $parsed_json_path = self::$fixtures_dir . '/' . $parsed_json_filename; - - foreach ( array( $html_path, $parsed_json_path ) as $filename ) { - if ( ! file_exists( $filename ) ) { - throw new Exception( "Missing fixture file: '$filename'" ); - } - } - - $html = self::strip_r( file_get_contents( $html_path ) ); - $expected_parsed = json_decode( self::strip_r( file_get_contents( $parsed_json_path ) ), true ); - - $parser = new WP_Block_Parser(); - $result = json_decode( json_encode( $parser->parse( $html ) ), true ); - - $this->assertEquals( - $expected_parsed, - $result, - "File '$parsed_json_filename' does not match expected value" - ); - } -} diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php deleted file mode 100644 index 3691c4d162af4f..00000000000000 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ /dev/null @@ -1,454 +0,0 @@ -user->create( - array( - 'role' => 'editor', - ) - ); - - self::$author_id = $factory->user->create( - array( - 'role' => 'author', - ) - ); - - self::$post_id = $factory->post->create( - array( - 'post_title' => 'Test Post', - ) - ); - } - - /** - * Delete test data after our tests run. - */ - public static function wpTearDownAfterClass() { - self::delete_user( self::$user_id ); - } - - /** - * Set up. - * - * @see gutenberg_register_rest_routes() - */ - public function setUp() { - $this->register_test_block(); - $this->register_post_context_test_block(); - parent::setUp(); - } - - /** - * Tear down. - */ - public function tearDown() { - WP_Block_Type_Registry::get_instance()->unregister( self::$block_name ); - WP_Block_Type_Registry::get_instance()->unregister( self::$context_block_name ); - parent::tearDown(); - } - - /** - * Register test block. - */ - public function register_test_block() { - register_block_type( - self::$block_name, - array( - 'attributes' => array( - 'some_string' => array( - 'type' => 'string', - 'default' => 'some_default', - ), - 'some_int' => array( - 'type' => 'integer', - ), - 'some_array' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ), - ), - 'render_callback' => array( $this, 'render_test_block' ), - ) - ); - } - - /** - * Register test block with post_id as attribute for post context test. - */ - public function register_post_context_test_block() { - register_block_type( - self::$context_block_name, - array( - 'attributes' => array(), - 'render_callback' => array( $this, 'render_post_context_test_block' ), - ) - ); - } - - /** - * Test render callback. - * - * @param array $attributes Props. - * @return string Rendered attributes, which is here just JSON. - */ - public function render_test_block( $attributes ) { - return wp_json_encode( $attributes ); - } - - /** - * Test render callback for testing post context. - * - * @return string - */ - public function render_post_context_test_block() { - return get_the_title(); - } - - /** - * Check that the route was registered properly. - * - * @covers WP_REST_Block_Renderer_Controller::register_routes() - */ - public function test_register_routes() { - $dynamic_block_names = get_dynamic_block_names(); - $this->assertContains( self::$block_name, $dynamic_block_names ); - - $routes = rest_get_server()->get_routes(); - foreach ( $dynamic_block_names as $dynamic_block_name ) { - $this->assertArrayHasKey( self::$rest_api_route . "(?P$dynamic_block_name)", $routes ); - } - } - - /** - * Test getting item without permissions. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_without_permissions() { - wp_set_current_user( 0 ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); - } - - /** - * Test getting item without 'edit' context. - */ - public function test_get_item_with_invalid_context() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Test getting item with invalid block name. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_invalid_block_name() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . 'core/123' ); - - $request->set_param( 'context', 'edit' ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'rest_no_route', $response, 404 ); - } - - /** - * Check getting item with an invalid param provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_invalid_attribute() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( - 'attributes', - array( - 'some_string' => array( 'no!' ), - ) - ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 400, $response->get_status() ); - } - - /** - * Check getting item with an invalid param provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_unrecognized_attribute() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( - 'attributes', - array( - 'unrecognized' => 'yes', - ) - ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 400, $response->get_status() ); - } - - /** - * Check getting item with default attributes provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_default_attributes() { - wp_set_current_user( self::$user_id ); - - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); - $defaults = array(); - foreach ( $block_type->attributes as $key => $attribute ) { - if ( isset( $attribute['default'] ) ) { - $defaults[ $key ] = $attribute['default']; - } - } - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', array() ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $defaults, json_decode( $data['rendered'], true ) ); - $this->assertEquals( - json_decode( $block_type->render( $defaults ) ), - json_decode( $data['rendered'] ) - ); - } - - /** - * Check getting item with attributes provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item() { - wp_set_current_user( self::$user_id ); - - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); - $attributes = array( - 'some_int' => '123', - 'some_string' => 'foo', - 'some_array' => array( 1, '2', 3 ), - ); - - $expected_attributes = $attributes; - $expected_attributes['some_int'] = (int) $expected_attributes['some_int']; - $expected_attributes['some_array'] = array_map( 'intval', $expected_attributes['some_array'] ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', $attributes ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $expected_attributes, json_decode( $data['rendered'], true ) ); - $this->assertEquals( - json_decode( $block_type->render( $attributes ), true ), - json_decode( $data['rendered'], true ) - ); - } - - - - /** - * Check success response for getting item with layout attribute provided. - */ - public function test_get_item_with_layout() { - wp_set_current_user( self::$user_id ); - - $attributes = array( - 'layout' => 'foo', - ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', $attributes ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - } - - /** - * Test getting item with post context. - */ - public function test_get_item_with_post_context() { - wp_set_current_user( self::$user_id ); - - $expected_title = 'Test Post'; - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test without post ID. - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertTrue( empty( $data['rendered'] ) ); - - // Now test with post ID. - $request->set_param( 'post_id', self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $expected_title, $data['rendered'] ); - } - - /** - * Test getting item with invalid post ID. - */ - public function test_get_item_without_permissions_invalid_post() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test with invalid post ID. - $request->set_param( 'post_id', PHP_INT_MAX ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); - } - - /** - * Test getting item without permissions to edit context post. - */ - public function test_get_item_without_permissions_cannot_edit_post() { - wp_set_current_user( self::$author_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test with private post ID. - $request->set_param( 'post_id', self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); - } - - /** - * Get item schema. - * - * @covers WP_REST_Block_Renderer_Controller::get_item_schema() - */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', self::$rest_api_route . self::$block_name ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertEqualSets( array( 'GET' ), $data['endpoints'][0]['methods'] ); - $this->assertEqualSets( - array( 'name', 'context', 'attributes', 'post_id' ), - array_keys( $data['endpoints'][0]['args'] ) - ); - $this->assertEquals( 'object', $data['endpoints'][0]['args']['attributes']['type'] ); - - $this->assertArrayHasKey( 'schema', $data ); - $this->assertEquals( 'rendered-block', $data['schema']['title'] ); - $this->assertEquals( 'object', $data['schema']['type'] ); - $this->arrayHasKey( 'rendered', $data['schema']['properties'] ); - $this->arrayHasKey( 'string', $data['schema']['properties']['rendered']['type'] ); - $this->assertEquals( array( 'edit' ), $data['schema']['properties']['rendered']['context'] ); - } - - public function test_update_item() { - $this->markTestSkipped( 'Controller doesn\'t implement update_item().' ); - } - - public function test_create_item() { - $this->markTestSkipped( 'Controller doesn\'t implement create_item().' ); - } - - public function test_delete_item() { - $this->markTestSkipped( 'Controller doesn\'t implement delete_item().' ); - } - - public function test_get_items() { - $this->markTestSkipped( 'Controller doesn\'t implement get_items().' ); - } - - public function test_context_param() { - $this->markTestSkipped( 'Controller doesn\'t implement context_param().' ); - } - - public function test_prepare_item() { - $this->markTestSkipped( 'Controller doesn\'t implement prepare_item().' ); - } -} diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php deleted file mode 100644 index 5a9e71af7e33e9..00000000000000 --- a/phpunit/class-rest-blocks-controller-test.php +++ /dev/null @@ -1,204 +0,0 @@ - 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '

Hello!

', - ) - ); - - self::$user_ids = array( - 'editor' => $factory->user->create( array( 'role' => 'editor' ) ), - 'author' => $factory->user->create( array( 'role' => 'author' ) ), - 'contributor' => $factory->user->create( array( 'role' => 'contributor' ) ), - ); - } - - /** - * Delete our fake data after our tests run. - */ - public static function wpTearDownAfterClass() { - wp_delete_post( self::$post_id ); - - foreach ( self::$user_ids as $user_id ) { - self::delete_user( $user_id ); - } - } - - /** - * Test cases for test_capabilities(). - */ - public function data_capabilities() { - return array( - array( 'create', 'editor', 201 ), - array( 'create', 'author', 201 ), - array( 'create', 'contributor', 403 ), - array( 'create', null, 401 ), - - array( 'read', 'editor', 200 ), - array( 'read', 'author', 200 ), - array( 'read', 'contributor', 200 ), - array( 'read', null, 401 ), - - array( 'update_delete_own', 'editor', 200 ), - array( 'update_delete_own', 'author', 200 ), - array( 'update_delete_own', 'contributor', 403 ), - - array( 'update_delete_others', 'editor', 200 ), - array( 'update_delete_others', 'author', 403 ), - array( 'update_delete_others', 'contributor', 403 ), - array( 'update_delete_others', null, 401 ), - ); - } - - /** - * Exhaustively check that each role either can or cannot create, edit, - * update, and delete reusable blocks. - * - * @dataProvider data_capabilities - */ - public function test_capabilities( $action, $role, $expected_status ) { - if ( $role ) { - $user_id = self::$user_ids[ $role ]; - wp_set_current_user( $user_id ); - } else { - wp_set_current_user( 0 ); - } - - switch ( $action ) { - case 'create': - $request = new WP_REST_Request( 'POST', '/wp/v2/blocks' ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'read': - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'update_delete_own': - $post_id = wp_insert_post( - array( - 'post_type' => 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '

Hello!

', - 'post_author' => $user_id, - ) - ); - - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . $post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - wp_delete_post( $post_id ); - - break; - - case 'update_delete_others': - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - default: - $this->fail( "'$action' is not a valid action." ); - } - } - - /** - * Check that the raw title and content of a block can be accessed when there - * is no set schema, and that the rendered content of a block is not included - * in the response. - */ - public function test_content() { - wp_set_current_user( self::$user_ids['author'] ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertEquals( - array( - 'raw' => 'My cool block', - ), - $data['title'] - ); - $this->assertEquals( - array( - 'raw' => '

Hello!

', - 'protected' => false, - ), - $data['content'] - ); - } -} diff --git a/phpunit/class-rest-search-controller-test.php b/phpunit/class-rest-search-controller-test.php deleted file mode 100644 index 26649037e8ff22..00000000000000 --- a/phpunit/class-rest-search-controller-test.php +++ /dev/null @@ -1,521 +0,0 @@ -post->create_many( - 4, - array( - 'post_title' => 'my-footitle', - 'post_type' => 'post', - ) - ); - - self::$my_title_page_ids = $factory->post->create_many( - 4, - array( - 'post_title' => 'my-footitle', - 'post_type' => 'page', - ) - ); - - self::$my_content_post_ids = $factory->post->create_many( - 6, - array( - 'post_content' => 'my-foocontent', - ) - ); - } - - /** - * Delete our fake data after our tests run. - */ - public static function wpTearDownAfterClass() { - $post_ids = array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ); - - foreach ( $post_ids as $post_id ) { - wp_delete_post( $post_id, true ); - } - } - - /** - * Check that our routes get set up properly. - */ - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - - $this->assertArrayHasKey( '/wp/v2/search', $routes ); - $this->assertCount( 1, $routes['/wp/v2/search'] ); - } - - /** - * Check the context parameter. - */ - public function test_context_param() { - $response = $this->do_request_with_params( array(), 'OPTIONS' ); - $data = $response->get_data(); - - $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertEquals( array( 'view', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); - } - - /** - * Search through all content. - */ - public function test_get_items() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all content with a low limit. - */ - public function test_get_items_with_limit() { - $response = $this->do_request_with_params( - array( - 'per_page' => 3, - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 3, count( $response->get_data() ) ); - } - - /** - * Search through posts of any post type. - */ - public function test_get_items_search_type_post() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through posts of post type 'post'. - */ - public function test_get_items_search_type_post_subtype_post() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'post', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through posts of post type 'page'. - */ - public function test_get_items_search_type_post_subtype_page() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'page', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - self::$my_title_page_ids, - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through an invalid type - */ - public function test_get_items_search_type_invalid() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'invalid', - ) - ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Search through posts of an invalid post type. - */ - public function test_get_items_search_type_post_subtype_invalid() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'invalid', - ) - ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Search through posts and pages. - */ - public function test_get_items_search_posts_and_pages() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'post,page', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all that matches a 'footitle' search. - */ - public function test_get_items_search_for_footitle() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'search' => 'footitle', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all that matches a 'foocontent' search. - */ - public function test_get_items_search_for_foocontent() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'search' => 'foocontent', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - self::$my_content_post_ids, - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Test retrieving a single item isn't possible. - */ - public function test_get_item() { - /** The search controller does not allow getting individual item content */ - $request = new WP_REST_Request( 'GET', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test creating an item isn't possible. - */ - public function test_create_item() { - /** The search controller does not allow creating content */ - $request = new WP_REST_Request( 'POST', '/wp/v2/search' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test updating an item isn't possible. - */ - public function test_update_item() { - /** The search controller does not allow upading content */ - $request = new WP_REST_Request( 'POST', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test deleting an item isn't possible. - */ - public function test_delete_item() { - /** The search controller does not allow deleting content */ - $request = new WP_REST_Request( 'DELETE', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test preparing the data contains the correct fields. - */ - public function test_prepare_item() { - $response = $this->do_request_with_params(); - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - 'url', - 'type', - 'subtype', - '_links', - ), - array_keys( $data[0] ) - ); - } - - /** - * Test preparing the data with limited fields contains the correct fields. - */ - public function test_prepare_item_limit_fields() { - if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { - $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); - } - - $response = $this->do_request_with_params( - array( - '_fields' => 'id,title', - ) - ); - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - '_links', - ), - array_keys( $data[0] ) - ); - } - - /** - * Tests the item schema is correct. - */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/search' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $properties = $data['schema']['properties']; - - $this->assertArrayHasKey( 'id', $properties ); - $this->assertArrayHasKey( 'title', $properties ); - $this->assertArrayHasKey( 'url', $properties ); - $this->assertArrayHasKey( 'type', $properties ); - $this->assertArrayHasKey( 'subtype', $properties ); - } - - /** - * Tests that non-public post types are not allowed. - */ - public function test_non_public_post_type() { - $response = $this->do_request_with_params( - array( - 'type' => 'post', - 'subtype' => 'post,nav_menu_item', - ) - ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Test getting items directly with a custom search handler. - */ - public function test_custom_search_handler_get_items() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'page' => 1, - 'per_page' => 10, - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - ) - ); - $response = $controller->get_items( $request ); - $this->assertEqualSets( range( 1, 10 ), wp_list_pluck( $response->get_data(), 'id' ) ); - - $request = $this->get_request( - array( - 'page' => 1, - 'per_page' => 10, - 'type' => 'dummy', - 'subtype' => array( 'dummy_first_type' ), - ) - ); - $response = $controller->get_items( $request ); - $this->assertEqualSets( range( 1, 5 ), wp_list_pluck( $response->get_data(), 'id' ) ); - } - - /** - * Test preparing an item directly with a custom search handler. - */ - public function test_custom_search_handler_prepare_item() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - ) - ); - $response = $controller->prepare_item_for_response( 1, $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - 'url', - 'type', - 'subtype', - ), - array_keys( $data ) - ); - } - - /** - * Test preparing an item directly with a custom search handler with limited fields. - */ - public function test_custom_search_handler_prepare_item_limit_fields() { - if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { - $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); - } - - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - '_fields' => 'id,title', - ) - ); - $response = $controller->prepare_item_for_response( 1, $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - ), - array_keys( $data ) - ); - } - - /** - * Test getting the collection params directly with a custom search handler. - */ - public function test_custom_search_handler_get_collection_params() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $params = $controller->get_collection_params(); - $this->assertEquals( 'dummy', $params[ WP_REST_Search_Controller::PROP_TYPE ]['default'] ); - $this->assertEqualSets( array( 'dummy' ), $params[ WP_REST_Search_Controller::PROP_TYPE ]['enum'] ); - $this->assertEqualSets( array( 'dummy_first_type', 'dummy_second_type', WP_REST_Search_Controller::TYPE_ANY ), $params[ WP_REST_Search_Controller::PROP_SUBTYPE ]['items']['enum'] ); - } - - /** - * Perform a REST request to our search endpoint with given parameters. - */ - private function do_request_with_params( $params = array(), $method = 'GET' ) { - $request = $this->get_request( $params, $method ); - - return rest_get_server()->dispatch( $request ); - } - - /** - * Get a REST request object for given parameters. - */ - private function get_request( $params = array(), $method = 'GET' ) { - $request = new WP_REST_Request( $method, '/wp/v2/search' ); - - foreach ( $params as $param => $value ) { - $request->set_param( $param, $value ); - } - - return $request; - } -} diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap index e911f407edd680..275080b436f2da 100644 --- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap +++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap @@ -24,6 +24,12 @@ exports[`RichText should handle change in tag name gracefully 1`] = ` " `; +exports[`RichText should only mutate text data on input 1`] = ` +" +

1234

+" +`; + exports[`RichText should transform backtick to code 1`] = ` "

A backtick

diff --git a/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap b/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap new file mode 100644 index 00000000000000..485862873b648d --- /dev/null +++ b/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WP Editor Meta Boxes Should save the changes 1`] = `"

Typing in a metabox

"`; diff --git a/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap b/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap new file mode 100644 index 00000000000000..a4461344bb4384 --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Classic should be inserted 1`] = `"test"`; diff --git a/test/e2e/specs/blocks/classic.test.js b/test/e2e/specs/blocks/classic.test.js new file mode 100644 index 00000000000000..f0b733b4a9947d --- /dev/null +++ b/test/e2e/specs/blocks/classic.test.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import uuid from 'uuid/v4'; + +/** + * Internal dependencies + */ +import { + getEditedPostContent, + newPost, + insertBlock, + pressWithModifier, +} from '../../support/utils'; + +describe( 'Classic', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'should be inserted', async () => { + await insertBlock( 'Classic' ); + // Wait for TinyMCE to initialise. + await page.waitForSelector( '.mce-content-body' ); + // Ensure there is focus. + await page.focus( '.mce-content-body' ); + await page.keyboard.type( 'test' ); + // Move focus away. + await pressWithModifier( 'shift', 'Tab' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should insert media', async () => { + await insertBlock( 'Classic' ); + // Wait for TinyMCE to initialise. + await page.waitForSelector( '.mce-content-body' ); + // Ensure there is focus. + await page.focus( '.mce-content-body' ); + await page.keyboard.type( 'test' ); + + // Click the image button. + await page.waitForSelector( 'div[aria-label="Add Media"]' ); + await page.click( 'div[aria-label="Add Media"]' ); + + // Wait for media modal to appear and upload image. + await page.waitForSelector( '.media-modal input[type=file]' ); + const inputElement = await page.$( '.media-modal input[type=file]' ); + const testImagePath = path.join( __dirname, '..', '..', 'assets', '10x10_e2e_test_image_z9T8jK.png' ); + const filename = uuid(); + const tmpFileName = path.join( os.tmpdir(), filename + '.png' ); + fs.copyFileSync( testImagePath, tmpFileName ); + await inputElement.uploadFile( tmpFileName ); + + // Wait for upload. + await page.waitForSelector( `.media-modal li[aria-label="${ filename }"]` ); + + // Insert the uploaded image. + await page.click( '.media-modal button.media-button-insert' ); + + // Wait for image to be inserted. + await page.waitForSelector( '.mce-content-body img' ); + + // Move focus away. + await pressWithModifier( 'shift', 'Tab' ); + + const regExp = new RegExp( `test` ); + expect( await getEditedPostContent() ).toMatch( regExp ); + } ); +} ); diff --git a/test/e2e/specs/classic-editor.test.js b/test/e2e/specs/classic-editor.test.js deleted file mode 100644 index 0d51c93e5b3420..00000000000000 --- a/test/e2e/specs/classic-editor.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import { visitAdmin } from '../support/utils'; - -describe( 'classic editor', () => { - beforeAll( async () => { - await visitAdmin( 'post-new.php', 'classic-editor' ); - } ); - - it( 'Should work properly', async () => { - // Click visual editor - await expect( page ).toClick( '#content-tmce' ); - await expect( page ).toClick( '#content_ifr' ); - - // type some random text - await page.keyboard.type( 'Typing in classic editor' ); - - // Switch to HTML mode - await expect( page ).toClick( '#content-html' ); - - const textEditorContent = await page.$eval( '.wp-editor-area', ( element ) => element.value ); - expect( textEditorContent ).toEqual( 'Typing in classic editor' ); - } ); -} ); diff --git a/test/e2e/specs/font-size-picker.test.js b/test/e2e/specs/font-size-picker.test.js index 4a55dd3e0dbc83..42bfe7e17f5a49 100644 --- a/test/e2e/specs/font-size-picker.test.js +++ b/test/e2e/specs/font-size-picker.test.js @@ -5,6 +5,7 @@ import { clickBlockAppender, getEditedPostContent, newPost, + pressTimes, } from '../support/utils'; describe( 'Font Size Picker', () => { @@ -32,7 +33,8 @@ describe( 'Font Size Picker', () => { await page.keyboard.type( 'Paragraph to be made "small"' ); await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.type( '13' ); + // This should be the "small" font-size of the current theme. + await page.keyboard.type( '19.5' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -58,7 +60,8 @@ describe( 'Font Size Picker', () => { await page.keyboard.type( 'Paragraph with font size reset using button' ); await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.type( '13' ); + // This should be the default font-size of the current theme. + await page.keyboard.type( '22' ); // Blur the range control await page.click( '.components-base-control__label' ); @@ -80,9 +83,10 @@ describe( 'Font Size Picker', () => { const changeSizeButton = await page.waitForSelector( '.components-button.is-font-large' ); await changeSizeButton.click(); + // Clear the custom font size input. await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.press( 'Backspace' ); + await pressTimes( 'ArrowRight', 4 ); + await pressTimes( 'Backspace', 4 ); // Ensure content matches snapshot. const content = await getEditedPostContent(); diff --git a/test/e2e/specs/new-post.test.js b/test/e2e/specs/new-post.test.js index da171371330db4..44e08f1902f2dd 100644 --- a/test/e2e/specs/new-post.test.js +++ b/test/e2e/specs/new-post.test.js @@ -2,12 +2,19 @@ * Internal dependencies */ import { newPost } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; describe( 'new editor state', () => { beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-post-formats-support' ); await newPost(); } ); + afterAll( async () => { + await newPost(); + await deactivatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + } ); + it( 'should show the New Post page in Gutenberg', async () => { expect( page.url() ).toEqual( expect.stringContaining( 'post-new.php' ) ); // Should display the blank title. diff --git a/test/e2e/specs/rich-text.test.js b/test/e2e/specs/rich-text.test.js index dca248c8289314..2f1012766444fd 100644 --- a/test/e2e/specs/rich-text.test.js +++ b/test/e2e/specs/rich-text.test.js @@ -67,4 +67,75 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should only mutate text data on input', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1' ); + await pressWithModifier( 'primary', 'b' ); + await page.keyboard.type( '2' ); + await pressWithModifier( 'primary', 'b' ); + await page.keyboard.type( '3' ); + + await page.evaluate( () => { + let called; + const { body } = document; + const config = { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }; + + const mutationObserver = new MutationObserver( ( records ) => { + if ( called || records.length > 1 ) { + throw new Error( 'Typing should only mutate once.' ); + } + + records.forEach( ( record ) => { + if ( record.type !== 'characterData' ) { + throw new Error( + `Typing mutated more than character data: ${ record.type }` + ); + } + } ); + + called = true; + } ); + + mutationObserver.observe( body, config ); + + window.unsubscribes = [ () => mutationObserver.disconnect() ]; + + document.addEventListener( 'selectionchange', () => { + function throwMultipleSelectionChange() { + throw new Error( 'Typing should only emit one selection change event.' ); + } + + document.addEventListener( + 'selectionchange', + throwMultipleSelectionChange, + { once: true } + ); + + window.unsubscribes.push( () => { + document.removeEventListener( 'selectionchange', throwMultipleSelectionChange ); + } ); + }, { once: true } ); + } ); + + await page.keyboard.type( '4' ); + + await page.evaluate( () => { + // The selection change event should be called once. If there's only + // one item in `window.unsubscribes`, it means that only one + // function is present to disconnect the `mutationObserver`. + if ( window.unsubscribes.length === 1 ) { + throw new Error( 'The selection change event listener was never called.' ); + } + + window.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() ); + } ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/test/e2e/specs/templates.test.js b/test/e2e/specs/templates.test.js index 9b5d22080e0d42..4d8d16e9ce9513 100644 --- a/test/e2e/specs/templates.test.js +++ b/test/e2e/specs/templates.test.js @@ -72,8 +72,14 @@ describe( 'templates', () => { await switchToTestUser(); } - beforeAll( async () => await setPostFormat( 'image' ) ); - afterAll( async () => await setPostFormat( STANDARD_FORMAT_VALUE ) ); + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + await setPostFormat( 'image' ); + } ); + afterAll( async () => { + await setPostFormat( STANDARD_FORMAT_VALUE ); + await deactivatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + } ); it( 'should populate new post with default block for format', async () => { await newPost(); diff --git a/test/e2e/specs/wp-editor-meta-box.test.js b/test/e2e/specs/wp-editor-meta-box.test.js new file mode 100644 index 00000000000000..496057bb55c1cf --- /dev/null +++ b/test/e2e/specs/wp-editor-meta-box.test.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import { newPost, publishPost } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; + +describe( 'WP Editor Meta Boxes', () => { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-wp-editor-meta-box' ); + await newPost(); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-plugin-wp-editor-meta-box' ); + } ); + + it( 'Should save the changes', async () => { + // Add title to enable valid non-empty post save. + await page.type( '.editor-post-title__input', 'Hello Meta' ); + + // Type something + await page.click( '#test_tinymce_id-html' ); + await page.type( '#test_tinymce_id', 'Typing in a metabox' ); + await page.click( '#test_tinymce_id-tmce' ); + + await publishPost(); + + await page.reload(); + + await page.click( '#test_tinymce_id-html' ); + const content = await page.$eval( + '#test_tinymce_id', + ( textarea ) => textarea.value + ); + + expect( content ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index bcad1f3d55bb0c..7c4197755bab81 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -44,6 +44,8 @@ describe( 'adding blocks', () => { // Arrow up in inner blocks should navigate through (1) column wrapper, // (2) text fields. + // We need to arrow up key presses in the paragraph block because it shows up in two lines. + await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); activeElementText = await page.evaluate( () => document.activeElement.textContent ); @@ -53,6 +55,7 @@ describe( 'adding blocks', () => { // columns wrappers before escaping out. let activeElementBlockType; await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); activeElementBlockType = await page.evaluate( () => ( document.activeElement.getAttribute( 'data-type' ) ) ); diff --git a/test/e2e/support/plugins.js b/test/e2e/support/plugins.js index da9edd3656860f..f8d64a7a1ec175 100644 --- a/test/e2e/support/plugins.js +++ b/test/e2e/support/plugins.js @@ -11,7 +11,7 @@ import { visitAdmin, switchToAdminUser, switchToTestUser } from './utils'; */ export async function installPlugin( slug, searchTerm ) { await switchToAdminUser(); - await visitAdmin( 'plugin-install.php?s=' + encodeURIComponent( searchTerm || slug ) + '&tab=search&type=term' ); + await visitAdmin( 'plugin-install.php', 's=' + encodeURIComponent( searchTerm || slug ) + '&tab=search&type=term' ); await page.click( '.install-now[data-slug="' + slug + '"]' ); await page.waitForSelector( '.activate-now[data-slug="' + slug + '"]' ); await switchToTestUser(); diff --git a/test/e2e/test-plugins/post-formats.php b/test/e2e/test-plugins/post-formats.php new file mode 100644 index 00000000000000..b0fdec68947fcb --- /dev/null +++ b/test/e2e/test-plugins/post-formats.php @@ -0,0 +1,15 @@ +ID, 'test_tinymce', true ); + wp_editor( $field_value, 'test_tinymce_id', array( + 'wpautop' => true, + 'media_buttons' => false, + 'textarea_name' => 'test_tinymce', + 'textarea_rows' => 10, + 'teeny' => true + ) ); + }, null, 'advanced', 'high' ); +}); +add_action( 'save_post', function( $post_id ){ + if ( ! isset( $_POST['test_tinymce'] ) ) { + return; + } + update_post_meta( $post_id, 'test_tinymce', $_POST['test_tinymce'] ); +});