} */
+ const chunkDynamicDeps = new Set();
+
if ( injectPolyfill ) {
- chunkDeps.add( 'wp-polyfill' );
+ chunkStaticDeps.add( 'wp-polyfill' );
}
- const processModule = ( { userRequest } ) => {
+ /**
+ * @param {webpack.Module} m
+ */
+ const processModule = ( m ) => {
+ const { userRequest } = m;
if ( this.externalizedDeps.has( userRequest ) ) {
- chunkDeps.add( this.mapRequestToDependency( userRequest ) );
+ if ( this.useModules ) {
+ const isStatic =
+ DependencyExtractionWebpackPlugin.hasStaticDependencyPathToRoot(
+ compilation,
+ m
+ );
+
+ ( isStatic ? chunkStaticDeps : chunkDynamicDeps ).add(
+ m.request
+ );
+ } else {
+ chunkStaticDeps.add(
+ this.mapRequestToDependency( userRequest )
+ );
+ }
}
};
// Search for externalized modules in all chunks.
- const modulesIterable =
- compilation.chunkGraph.getChunkModules( chunk );
- for ( const chunkModule of modulesIterable ) {
+ for ( const chunkModule of compilation.chunkGraph.getChunkModulesIterable(
+ chunk
+ ) ) {
processModule( chunkModule );
// Loop through submodules of ConcatenatedModule.
if ( chunkModule.modules ) {
@@ -209,11 +274,20 @@ class DependencyExtractionWebpackPlugin {
.slice( 0, hashDigestLength );
const assetData = {
- // Get a sorted array so we can produce a stable, stringified representation.
- dependencies: Array.from( chunkDeps ).sort(),
+ dependencies: [
+ // Sort these so we can produce a stable, stringified representation.
+ ...Array.from( chunkStaticDeps ).sort(),
+ ...Array.from( chunkDynamicDeps )
+ .sort()
+ .map( ( id ) => ( { id, type: 'dynamic' } ) ),
+ ],
version: contentHash,
};
+ if ( this.useModules ) {
+ assetData.type = 'module';
+ }
+
if ( combineAssets ) {
combinedAssetsData[ chunkJSFile ] = assetData;
continue;
@@ -231,7 +305,7 @@ class DependencyExtractionWebpackPlugin {
'.asset.' + ( outputFormat === 'php' ? 'php' : 'json' );
assetFilename = compilation
.getPath( '[file]', { filename: chunkJSFile } )
- .replace( /\.js$/i, suffix );
+ .replace( /\.m?js$/i, suffix );
}
// Add source and file into compilation for webpack to output.
@@ -260,6 +334,58 @@ class DependencyExtractionWebpackPlugin {
);
}
}
+
+ /**
+ * Can we trace a line of static dependencies from an entry to a module
+ *
+ * @param {webpack.Compilation} compilation
+ * @param {webpack.DependenciesBlock} block
+ *
+ * @return {boolean} True if there is a static import path to the root
+ */
+ static hasStaticDependencyPathToRoot( compilation, block ) {
+ const incomingConnections = [
+ ...compilation.moduleGraph.getIncomingConnections( block ),
+ ].filter(
+ ( connection ) =>
+ // Library connections don't have a dependency, this is a root
+ connection.dependency &&
+ // Entry dependencies are another root
+ connection.dependency.constructor.name !== 'EntryDependency'
+ );
+
+ // If we don't have non-entry, non-library incoming connections,
+ // we've reached a root of
+ if ( ! incomingConnections.length ) {
+ return true;
+ }
+
+ const staticDependentModules = incomingConnections.flatMap(
+ ( connection ) => {
+ const { dependency } = connection;
+ const parentBlock =
+ compilation.moduleGraph.getParentBlock( dependency );
+
+ return parentBlock.constructor.name !==
+ AsyncDependenciesBlock.name
+ ? [ compilation.moduleGraph.getParentModule( dependency ) ]
+ : [];
+ }
+ );
+
+ // All the dependencies were Async, the module was reached via a dynamic import
+ if ( ! staticDependentModules.length ) {
+ return false;
+ }
+
+ // Continue to explore any static dependencies
+ return staticDependentModules.some( ( parentStaticDependentModule ) =>
+ DependencyExtractionWebpackPlugin.hasStaticDependencyPathToRoot(
+ compilation,
+ parentStaticDependentModule
+ )
+ );
+ }
}
module.exports = DependencyExtractionWebpackPlugin;
diff --git a/packages/dependency-extraction-webpack-plugin/lib/types.d.ts b/packages/dependency-extraction-webpack-plugin/lib/types.d.ts
index 179b4dab593bd6..c4a4af52b1b2fc 100644
--- a/packages/dependency-extraction-webpack-plugin/lib/types.d.ts
+++ b/packages/dependency-extraction-webpack-plugin/lib/types.d.ts
@@ -13,6 +13,7 @@ declare interface DependencyExtractionWebpackPluginOptions {
outputFormat?: 'php' | 'json';
outputFilename?: string | Function;
requestToExternal?: ( request: string ) => string | string[] | undefined;
+ requestToExternalModule?: ( request: string ) => string | undefined;
requestToHandle?: ( request: string ) => string | undefined;
combinedOutputFile?: string | null;
combineAssets?: boolean;
diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js
index bd328430313ce9..bc2b2221e8fc9a 100644
--- a/packages/dependency-extraction-webpack-plugin/lib/util.js
+++ b/packages/dependency-extraction-webpack-plugin/lib/util.js
@@ -1,10 +1,10 @@
const WORDPRESS_NAMESPACE = '@wordpress/';
const BUNDLED_PACKAGES = [
+ '@wordpress/dataviews',
'@wordpress/icons',
'@wordpress/interface',
- '@wordpress/undo-manager',
'@wordpress/sync',
- '@wordpress/dataviews',
+ '@wordpress/undo-manager',
];
/**
@@ -56,6 +56,21 @@ function defaultRequestToExternal( request ) {
}
}
+/**
+ * Default request to external module transformation
+ *
+ * Currently only @wordpress/interactivity
+ *
+ * @param {string} request Module request (the module name in `import from`) to be transformed
+ * @return {string|undefined} The resulting external definition. Return `undefined`
+ * to ignore the request. Return `string` to map the request to an external. This may simply be returning the request, e.g. `@wordpress/interactivity` maps to the external `@wordpress/interactivity`.
+ */
+function defaultRequestToExternalModule( request ) {
+ if ( request === '@wordpress/interactivity' ) {
+ return request;
+ }
+}
+
/**
* Default request to WordPress script handle transformation
*
@@ -101,5 +116,6 @@ function camelCaseDash( string ) {
module.exports = {
camelCaseDash,
defaultRequestToExternal,
+ defaultRequestToExternalModule,
defaultRequestToHandle,
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
index 4240c2f2ea3782..3c8f89fc14ee92 100644
--- a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
+++ b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
@@ -1,5 +1,283 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`combine-assets\` should produce expected output: Asset file 'assets.php' should match snapshot 1`] = `
+" array('dependencies' => array('@wordpress/blob'), 'version' => '8652d2bf4a1ea1969a6e', 'type' => 'module'), 'fileB.mjs' => array('dependencies' => array('@wordpress/token-list'), 'version' => '17d7d5b2c152592ff3a0', 'type' => 'module'));
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`combine-assets\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+ {
+ "externalType": "module",
+ "request": "@wordpress/token-list",
+ "userRequest": "@wordpress/token-list",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-dependency-graph\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/interactivity'), 'version' => '58fadee5eca3ad30aff6', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-dependency-graph\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/interactivity",
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-dynamic-dependency-graph\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array(array('id' => '@wordpress/interactivity', 'type' => 'dynamic')), 'version' => '293aebad4ca761cf396f', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-dynamic-dependency-graph\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/interactivity",
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`dynamic-import\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array(array('id' => '@wordpress/blob', 'type' => 'dynamic')), 'version' => '092c2bce8c247ee11100', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`dynamic-import\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`function-output-filename\` should produce expected output: Asset file 'chunk--main--main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '5207bcd3fdd29de25f37', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`function-output-filename\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`has-extension-suffix\` should produce expected output: Asset file 'index.min.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '9b89a3e6236b26559c4e', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`has-extension-suffix\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`module-renames\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('renamed--@my/module', 'renamed--other-module'), 'version' => '601cf94eb9a182fcc0ed', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`module-renames\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "renamed--@my/module",
+ "userRequest": "@my/module",
+ },
+ {
+ "externalType": "module",
+ "request": "renamed--other-module",
+ "userRequest": "other-module",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`no-default\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array(), 'version' => '34504aa793c63cd3d73a', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`no-default\` should produce expected output: External modules should match snapshot 1`] = `[]`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`no-deps\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array(), 'version' => 'e37fbd452a6188261d74', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`no-deps\` should produce expected output: External modules should match snapshot 1`] = `[]`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`option-function-output-filename\` should produce expected output: Asset file 'chunk--main--main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '5207bcd3fdd29de25f37', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`option-function-output-filename\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`option-output-filename\` should produce expected output: Asset file 'main-foo.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '5207bcd3fdd29de25f37', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`option-output-filename\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`output-format-json\` should produce expected output: Asset file 'main.asset.json' should match snapshot 1`] = `"{"dependencies":[],"version":"34504aa793c63cd3d73a","type":"module"}"`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`output-format-json\` should produce expected output: External modules should match snapshot 1`] = `[]`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`overrides\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob', '@wordpress/url', 'rxjs', 'rxjs/operators'), 'version' => '90f2e6327f4e8fb0264f', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`overrides\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+ {
+ "externalType": "module",
+ "request": "@wordpress/url",
+ "userRequest": "@wordpress/url",
+ },
+ {
+ "externalType": "module",
+ "request": "rxjs",
+ "userRequest": "rxjs",
+ },
+ {
+ "externalType": "module",
+ "request": "rxjs/operators",
+ "userRequest": "rxjs/operators",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'a.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => 'aeadada5bf49ae3b9dc2', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'b.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '10df52cc859c01faa91d', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'runtime.asset.php' should match snapshot 1`] = `
+" array(), 'version' => 'd081f44e5ece6763f943', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`runtime-chunk-single\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`style-imports\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '2d597a618aeebe7ab323', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`style-imports\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '5207bcd3fdd29de25f37', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress-interactivity\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array(array('id' => '@wordpress/interactivity', 'type' => 'dynamic')), 'version' => 'f0242eb6da78af6ca4b8', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress-interactivity\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/interactivity",
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress-require\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('@wordpress/blob'), 'version' => '7a320492a2396d955292', 'type' => 'module');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin modules Webpack \`wordpress-require\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "module",
+ "request": "@wordpress/blob",
+ "userRequest": "@wordpress/blob",
+ },
+]
+`;
+
exports[`DependencyExtractionWebpackPlugin scripts Webpack \`combine-assets\` should produce expected output: Asset file 'assets.php' should match snapshot 1`] = `
" array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'cbe985cf6e1a25d848e5'), 'fileB.js' => array('dependencies' => array('wp-token-list'), 'version' => '7f3970305cf0aecb54ab'));
"
@@ -31,6 +309,42 @@ exports[`DependencyExtractionWebpackPlugin scripts Webpack \`combine-assets\` sh
]
`;
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-dependency-graph\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('wp-interactivity'), 'version' => '79a1af3afac581f52492');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-dependency-graph\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "window",
+ "request": [
+ "wp",
+ "interactivity",
+ ],
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-dynamic-dependency-graph\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('wp-interactivity'), 'version' => 'ac0e2f1bcd3a6a0e7aff');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-dynamic-dependency-graph\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "window",
+ "request": [
+ "wp",
+ "interactivity",
+ ],
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
exports[`DependencyExtractionWebpackPlugin scripts Webpack \`dynamic-import\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
" array('wp-blob'), 'version' => 'c0e8a6f22065ea096606');
"
@@ -95,6 +409,32 @@ exports[`DependencyExtractionWebpackPlugin scripts Webpack \`has-extension-suffi
]
`;
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`module-renames\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('renamed--@my/module', 'renamed--other-module'), 'version' => '34854902f36ec8e176d6');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`module-renames\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "window",
+ "request": [
+ "my-namespace",
+ "renamed--@my/module",
+ ],
+ "userRequest": "@my/module",
+ },
+ {
+ "externalType": "window",
+ "request": [
+ "my-namespace",
+ "renamed--other-module",
+ ],
+ "userRequest": "other-module",
+ },
+]
+`;
+
exports[`DependencyExtractionWebpackPlugin scripts Webpack \`no-default\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
" array(), 'version' => '43880e6c42e7c39fcdf1');
"
@@ -285,6 +625,29 @@ exports[`DependencyExtractionWebpackPlugin scripts Webpack \`wordpress\` should
]
`;
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`wordpress-interactivity\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
+" array('lodash', 'wp-interactivity'), 'version' => 'b16015e38aea0509f75f');
+"
+`;
+
+exports[`DependencyExtractionWebpackPlugin scripts Webpack \`wordpress-interactivity\` should produce expected output: External modules should match snapshot 1`] = `
+[
+ {
+ "externalType": "window",
+ "request": "lodash",
+ "userRequest": "lodash",
+ },
+ {
+ "externalType": "window",
+ "request": [
+ "wp",
+ "interactivity",
+ ],
+ "userRequest": "@wordpress/interactivity",
+ },
+]
+`;
+
exports[`DependencyExtractionWebpackPlugin scripts Webpack \`wordpress-require\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = `
" array('lodash', 'wp-blob'), 'version' => '40370eb4ce6428562da6');
"
diff --git a/packages/dependency-extraction-webpack-plugin/test/build.js b/packages/dependency-extraction-webpack-plugin/test/build.js
index 7c84e4b0bcc8b3..3b29d55caf2bb0 100644
--- a/packages/dependency-extraction-webpack-plugin/test/build.js
+++ b/packages/dependency-extraction-webpack-plugin/test/build.js
@@ -13,89 +13,109 @@ const configFixtures = fs.readdirSync( fixturesPath ).sort();
afterAll( () => rimraf( path.join( __dirname, 'build' ) ) );
-describe( 'DependencyExtractionWebpackPlugin scripts', () => {
- describe.each( configFixtures )( 'Webpack `%s`', ( configCase ) => {
- const testDirectory = path.join( fixturesPath, configCase );
- const outputDirectory = path.join( __dirname, 'build', configCase );
+describe.each( /** @type {const} */ ( [ 'scripts', 'modules' ] ) )(
+ 'DependencyExtractionWebpackPlugin %s',
+ ( moduleMode ) => {
+ describe.each( configFixtures )( 'Webpack `%s`', ( configCase ) => {
+ const testDirectory = path.join( fixturesPath, configCase );
+ const outputDirectory = path.join(
+ __dirname,
+ 'build',
+ moduleMode,
+ configCase
+ );
- beforeEach( () => {
- rimraf( outputDirectory );
- mkdirp( outputDirectory );
- } );
+ beforeEach( () => {
+ rimraf( outputDirectory );
+ mkdirp( outputDirectory );
+ } );
- // This afterEach is necessary to prevent watched tests from retriggering on every run.
- afterEach( () => rimraf( outputDirectory ) );
+ // This afterEach is necessary to prevent watched tests from retriggering on every run.
+ afterEach( () => rimraf( outputDirectory ) );
- test( 'should produce expected output', async () => {
- const options = Object.assign(
- {
- target: 'web',
- context: testDirectory,
- entry: './index.js',
- mode: 'production',
- optimization: {
- minimize: false,
- chunkIds: 'named',
- moduleIds: 'named',
+ test( 'should produce expected output', async () => {
+ const options = Object.assign(
+ {
+ name: `${ configCase }-${ moduleMode }`,
+ target: 'web',
+ context: testDirectory,
+ entry: './index.js',
+ mode: 'production',
+ optimization: {
+ minimize: false,
+ chunkIds: 'named',
+ moduleIds: 'named',
+ },
+ output: {},
+ experiments: {},
},
- output: {},
- experiments: {},
- },
- require( path.join( testDirectory, 'webpack.config.js' ) )
- );
- options.output.path = outputDirectory;
+ require( path.join( testDirectory, 'webpack.config.js' ) )
+ );
+ options.output.path = outputDirectory;
- /** @type {webpack.Stats} */
- const stats = await new Promise( ( resolve, reject ) =>
- webpack( options, ( err, _stats ) => {
- if ( err ) {
- return reject( err );
- }
- resolve( _stats );
- } )
- );
+ if ( moduleMode === 'modules' ) {
+ options.target = 'es2024';
+ options.output.module = true;
+ options.output.chunkFormat = 'module';
+ options.output.library = options.output.library || {};
+ options.output.library.type = 'module';
+ options.experiments.outputModule = true;
+ }
- if ( stats.hasErrors() ) {
- throw new Error(
- stats.toString( { errors: true, all: false } )
+ /** @type {webpack.Stats} */
+ const stats = await new Promise( ( resolve, reject ) =>
+ webpack( options, ( err, _stats ) => {
+ if ( err ) {
+ return reject( err );
+ }
+ resolve( _stats );
+ } )
);
- }
- const assetFiles = glob(
- `${ outputDirectory }/+(*.asset|assets).@(json|php)`
- );
+ if ( stats.hasErrors() ) {
+ throw new Error(
+ stats.toString( { errors: true, all: false } )
+ );
+ }
- expect( assetFiles.length ).toBeGreaterThan( 0 );
+ const assetFiles = glob(
+ `${ outputDirectory }/+(*.asset|assets).@(json|php)`
+ );
- // Asset files should match.
- assetFiles.forEach( ( assetFile ) => {
- const assetBasename = path.basename( assetFile );
+ expect( assetFiles.length ).toBeGreaterThan( 0 );
- expect( fs.readFileSync( assetFile, 'utf-8' ) ).toMatchSnapshot(
- `Asset file '${ assetBasename }' should match snapshot`
- );
- } );
+ // Asset files should match.
+ assetFiles.forEach( ( assetFile ) => {
+ const assetBasename = path.basename( assetFile );
- const compareByModuleIdentifier = ( m1, m2 ) => {
- const i1 = m1.identifier();
- const i2 = m2.identifier();
- if ( i1 < i2 ) return -1;
- if ( i1 > i2 ) return 1;
- return 0;
- };
+ expect(
+ fs.readFileSync( assetFile, 'utf-8' )
+ ).toMatchSnapshot(
+ `Asset file '${ assetBasename }' should match snapshot`
+ );
+ } );
- // Webpack stats external modules should match.
- const externalModules = Array.from( stats.compilation.modules )
- .filter( ( { externalType } ) => externalType )
- .sort( compareByModuleIdentifier )
- .map( ( module ) => ( {
- externalType: module.externalType,
- request: module.request,
- userRequest: module.userRequest,
- } ) );
- expect( externalModules ).toMatchSnapshot(
- 'External modules should match snapshot'
- );
+ const compareByModuleIdentifier = ( m1, m2 ) => {
+ const i1 = m1.identifier();
+ const i2 = m2.identifier();
+ if ( i1 < i2 ) return -1;
+ if ( i1 > i2 ) return 1;
+ return 0;
+ };
+
+ // Webpack stats external modules should match.
+ const externalModules = Array.from( stats.compilation.modules )
+ .filter( ( { externalType } ) => externalType )
+ .sort( compareByModuleIdentifier )
+ .map( ( module ) => ( {
+ externalType: module.externalType,
+ request: module.request,
+ userRequest: module.userRequest,
+ } ) );
+ expect( externalModules ).toMatchSnapshot(
+ 'External modules should match snapshot'
+ );
+ } );
} );
- } );
-} );
+ }
+);
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/webpack.config.js
index 2ce7ba1be98e25..fb7ba94ca80998 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/webpack.config.js
@@ -11,6 +11,11 @@ module.exports = {
plugins: [
new DependencyExtractionWebpackPlugin( {
combineAssets: true,
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
} ),
],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/a.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/a.js
new file mode 100644
index 00000000000000..11dd7764ad5f92
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/a.js
@@ -0,0 +1,13 @@
+/**
+ * WordPress dependencies
+ */
+import { store } from '@wordpress/interactivity';
+
+/**
+ * Internal dependencies
+ */
+import { identity } from './b.js';
+
+identity( 1 );
+
+export { identity, store };
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/b.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/b.js
new file mode 100644
index 00000000000000..ce109acccbd370
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/b.js
@@ -0,0 +1,10 @@
+/**
+ * Internal dependencies
+ */
+import { store } from './a.js';
+
+export function identity( x ) {
+ return x;
+}
+
+export { store };
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/index.js
new file mode 100644
index 00000000000000..13b17a73ad4af8
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/index.js
@@ -0,0 +1,8 @@
+/**
+ * Internal dependencies
+ */
+import { identity as aIdentity, store as aStore } from './a.js';
+import { identity as bIdentity, store as bStore } from './b.js';
+
+aStore( aIdentity( 'a' ), { a: aIdentity( 'a' ) } );
+bStore( bIdentity( 'b' ), { b: bIdentity( 'b' ) } );
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/webpack.config.js
new file mode 100644
index 00000000000000..bfffff3ae78319
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/webpack.config.js
@@ -0,0 +1,8 @@
+/**
+ * Internal dependencies
+ */
+const DependencyExtractionWebpackPlugin = require( '../../..' );
+
+module.exports = {
+ plugins: [ new DependencyExtractionWebpackPlugin() ],
+};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/a.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/a.js
new file mode 100644
index 00000000000000..11dd7764ad5f92
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/a.js
@@ -0,0 +1,13 @@
+/**
+ * WordPress dependencies
+ */
+import { store } from '@wordpress/interactivity';
+
+/**
+ * Internal dependencies
+ */
+import { identity } from './b.js';
+
+identity( 1 );
+
+export { identity, store };
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/b.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/b.js
new file mode 100644
index 00000000000000..25a6aa127d26fd
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/b.js
@@ -0,0 +1,10 @@
+/**
+ * Internal dependencies
+ */
+const { store } = import( './a.js' );
+
+export function identity( x ) {
+ return x;
+}
+
+export { store };
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/index.js
new file mode 100644
index 00000000000000..073b4244dcea26
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/index.js
@@ -0,0 +1,9 @@
+/**
+ * Internal dependencies
+ */
+import { identity as bIdentity, store as bStore } from './b.js';
+
+const { identity: aIdentity, store: aStore } = await import( './a.js' );
+
+aStore( aIdentity( 'a' ), { a: aIdentity( 'a' ) } );
+bStore( bIdentity( 'b' ), { b: bIdentity( 'b' ) } );
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/webpack.config.js
new file mode 100644
index 00000000000000..bfffff3ae78319
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/webpack.config.js
@@ -0,0 +1,8 @@
+/**
+ * Internal dependencies
+ */
+const DependencyExtractionWebpackPlugin = require( '../../..' );
+
+module.exports = {
+ plugins: [ new DependencyExtractionWebpackPlugin() ],
+};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/dynamic-import/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/dynamic-import/webpack.config.js
index bfffff3ae78319..6856d328ab7c68 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/dynamic-import/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/dynamic-import/webpack.config.js
@@ -4,5 +4,13 @@
const DependencyExtractionWebpackPlugin = require( '../../..' );
module.exports = {
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/function-output-filename/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/function-output-filename/webpack.config.js
index 2d5b2e43b735ec..f637a4087e3ca3 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/function-output-filename/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/function-output-filename/webpack.config.js
@@ -9,5 +9,13 @@ module.exports = {
return `chunk--${ chunkData.chunk.name }--[name].js`;
},
},
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/webpack.config.js
index d814beacdf4dc3..ada40c8bf8e54e 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/webpack.config.js
@@ -7,5 +7,13 @@ module.exports = {
output: {
filename: 'index.min.js',
},
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/index.js
new file mode 100644
index 00000000000000..dc3702922c6ff4
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/index.js
@@ -0,0 +1,7 @@
+/**
+ * External dependencies
+ */
+import * as m from '@my/module';
+import { other } from 'other-module';
+
+m.load( other );
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/webpack.config.js
new file mode 100644
index 00000000000000..8b78e1fdea1505
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/webpack.config.js
@@ -0,0 +1,32 @@
+/**
+ * Internal dependencies
+ */
+const DependencyExtractionWebpackPlugin = require( '../../..' );
+
+module.exports = {
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternal( request ) {
+ switch ( request ) {
+ case '@my/module':
+ case 'other-module':
+ return [ 'my-namespace', `renamed--${ request }` ];
+ }
+ },
+ requestToHandle( request ) {
+ switch ( request ) {
+ case '@my/module':
+ case 'other-module':
+ return `renamed--${ request }`;
+ }
+ },
+ requestToExternalModule( request ) {
+ switch ( request ) {
+ case '@my/module':
+ case 'other-module':
+ return `renamed--${ request }`;
+ }
+ },
+ } ),
+ ],
+};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/option-function-output-filename/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/option-function-output-filename/webpack.config.js
index e328c817851ce1..5056f312c39992 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/option-function-output-filename/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/option-function-output-filename/webpack.config.js
@@ -9,6 +9,11 @@ module.exports = {
outputFilename( chunkData ) {
return `chunk--${ chunkData.chunk.name }--[name].asset.php`;
},
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
} ),
],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/option-output-filename/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/option-output-filename/webpack.config.js
index 9ec78f6437a18b..be52e661653868 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/option-output-filename/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/option-output-filename/webpack.config.js
@@ -7,6 +7,11 @@ module.exports = {
plugins: [
new DependencyExtractionWebpackPlugin( {
outputFilename: '[name]-foo.asset.php',
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
} ),
],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/overrides/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/overrides/webpack.config.js
index 9885e5cade7e96..89eaf6ee4b2f53 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/overrides/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/overrides/webpack.config.js
@@ -15,6 +15,18 @@ module.exports = {
return [ 'rxjs', 'operators' ];
}
},
+ requestToExternalModule( request ) {
+ if ( request === 'rxjs' ) {
+ return request;
+ }
+
+ if ( request === 'rxjs/operators' ) {
+ return request;
+ }
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
requestToHandle( request ) {
if ( request === 'rxjs' || request === 'rxjs/operators' ) {
return 'wp-script-handle-for-rxjs';
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/runtime-chunk-single/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/runtime-chunk-single/webpack.config.js
index e16f6b6b0fe70a..1e0824563c52f0 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/runtime-chunk-single/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/runtime-chunk-single/webpack.config.js
@@ -8,7 +8,15 @@ module.exports = {
a: './a',
b: './b',
},
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
optimization: {
runtimeChunk: 'single',
},
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js
index 52cb718a579de4..332e182e34b042 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js
@@ -10,7 +10,13 @@ const DependencyExtractionWebpackPlugin = require( '../../..' );
module.exports = {
plugins: [
- new DependencyExtractionWebpackPlugin(),
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
new MiniCSSExtractPlugin(),
],
module: {
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/index.js
new file mode 100644
index 00000000000000..b4dd2f288661ea
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/index.js
@@ -0,0 +1,12 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable eslint-comments/no-unlimited-disable */
+/* eslint-disable */
+
+import _ from 'lodash';
+
+// This module should be externalized
+const { store, getContext } = await import( '@wordpress/interactivity' );
+
+store( _.identity( 'my-namespace' ), { state: 'is great' } );
+
+getContext();
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/webpack.config.js
new file mode 100644
index 00000000000000..bfffff3ae78319
--- /dev/null
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/webpack.config.js
@@ -0,0 +1,8 @@
+/**
+ * Internal dependencies
+ */
+const DependencyExtractionWebpackPlugin = require( '../../..' );
+
+module.exports = {
+ plugins: [ new DependencyExtractionWebpackPlugin() ],
+};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-require/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-require/webpack.config.js
index bfffff3ae78319..6856d328ab7c68 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-require/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-require/webpack.config.js
@@ -4,5 +4,13 @@
const DependencyExtractionWebpackPlugin = require( '../../..' );
module.exports = {
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
};
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress/webpack.config.js
index bfffff3ae78319..6856d328ab7c68 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress/webpack.config.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress/webpack.config.js
@@ -4,5 +4,13 @@
const DependencyExtractionWebpackPlugin = require( '../../..' );
module.exports = {
- plugins: [ new DependencyExtractionWebpackPlugin() ],
+ plugins: [
+ new DependencyExtractionWebpackPlugin( {
+ requestToExternalModule( request ) {
+ if ( request.startsWith( '@wordpress/' ) ) {
+ return request;
+ }
+ },
+ } ),
+ ],
};
diff --git a/packages/e2e-tests/specs/editor/various/datepicker.test.js b/packages/e2e-tests/specs/editor/various/datepicker.test.js
deleted file mode 100644
index 6838fd56a2ba9a..00000000000000
--- a/packages/e2e-tests/specs/editor/various/datepicker.test.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { createNewPost, changeSiteTimezone } from '@wordpress/e2e-test-utils';
-
-async function getInputValue( selector ) {
- return page.$eval( selector, ( element ) => element.value );
-}
-
-async function getSelectedOptionLabel( selector ) {
- return page.$eval(
- selector,
- ( element ) => element.options[ element.selectedIndex ].text
- );
-}
-
-async function getDatePickerValues() {
- const year = await getInputValue(
- '.components-datetime__time-field-year input'
- );
- const month = await getInputValue(
- '.components-datetime__time-field-month select'
- );
- const monthLabel = await getSelectedOptionLabel(
- '.components-datetime__time-field-month select'
- );
- const day = await getInputValue(
- '.components-datetime__time-field-day input'
- );
- const hours = await getInputValue(
- '.components-datetime__time-field-hours-input input'
- );
- const minutes = await getInputValue(
- '.components-datetime__time-field-minutes-input input'
- );
- const amOrPm = await page.$eval(
- '.components-datetime__time-field-am-pm .is-primary',
- ( element ) => element.innerText.toLowerCase()
- );
-
- return { year, month, monthLabel, day, hours, minutes, amOrPm };
-}
-
-function trimLeadingZero( str ) {
- return str[ 0 ] === '0' ? str.slice( 1 ) : str;
-}
-
-function formatDatePickerValues(
- { year, monthLabel, day, hours, minutes, amOrPm },
- timezone
-) {
- const dayTrimmed = trimLeadingZero( day );
- const hoursTrimmed = trimLeadingZero( hours );
- return `${ monthLabel } ${ dayTrimmed }, ${ year } ${ hoursTrimmed }:${ minutes }\xa0${ amOrPm } ${ timezone }`;
-}
-
-async function getPublishingDate() {
- return page.$eval(
- '.editor-post-schedule__dialog-toggle',
- ( dateLabel ) => dateLabel.textContent
- );
-}
-
-describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )(
- `Datepicker %s`,
- ( timezone ) => {
- let oldTimezone;
- beforeEach( async () => {
- await page.emulateTimezone( 'America/New_York' ); // Set browser to a timezone that's different to `timezone`.
- oldTimezone = await changeSiteTimezone( timezone );
- await createNewPost();
- } );
- afterEach( async () => {
- await changeSiteTimezone( oldTimezone );
- await page.emulateTimezone( null );
- } );
-
- it( 'should show the publishing date as "Immediately" if the date is not altered', async () => {
- const publishingDate = await getPublishingDate();
-
- expect( publishingDate ).toEqual( 'Immediately' );
- } );
-
- it( 'should show the publishing date if the date is in the past', async () => {
- // Open the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- // Change the publishing date to a year in the past.
- await page.click( '.components-datetime__time-field-year' );
- await page.keyboard.press( 'ArrowDown' );
- const datePickerValues = await getDatePickerValues();
-
- // Close the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- const publishingDate = await getPublishingDate();
-
- expect( publishingDate ).toBe(
- formatDatePickerValues( datePickerValues, timezone )
- );
- } );
-
- it( 'should show the publishing date if the date is in the future', async () => {
- // Open the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- // Change the publishing date to a year in the future.
- await page.click( '.components-datetime__time-field-year' );
- await page.keyboard.press( 'ArrowUp' );
- const datePickerValues = await getDatePickerValues();
-
- // Close the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- const publishingDate = await getPublishingDate();
-
- expect( publishingDate ).not.toEqual( 'Immediately' );
- // The expected date format will be "Sep 26, 2018 11:52 pm".
- expect( publishingDate ).toBe(
- formatDatePickerValues( datePickerValues, timezone )
- );
- } );
-
- it( `should show the publishing date as "Immediately" if the date is cleared`, async () => {
- // Open the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- // Change the publishing date to a year in the future.
- await page.click( '.components-datetime__time-field-year' );
- await page.keyboard.press( 'ArrowUp' );
-
- // Close the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- // Open the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- // Clear the date.
- await page.click(
- '.block-editor-publish-date-time-picker button[aria-label="Now"]'
- );
-
- const publishingDate = await getPublishingDate();
-
- expect( publishingDate ).toEqual( 'Immediately' );
- } );
- }
-);
diff --git a/packages/e2e-tests/specs/editor/various/invalid-block.test.js b/packages/e2e-tests/specs/editor/various/invalid-block.test.js
deleted file mode 100644
index 354c370434be92..00000000000000
--- a/packages/e2e-tests/specs/editor/various/invalid-block.test.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- clickMenuItem,
- createNewPost,
- clickBlockAppender,
- clickBlockToolbarButton,
- setPostContent,
- canvas,
-} from '@wordpress/e2e-test-utils';
-
-describe( 'invalid blocks', () => {
- beforeEach( async () => {
- await createNewPost();
- } );
-
- it( 'Should show an invalid block message with clickable options', async () => {
- // Create an empty paragraph with the focus in the block.
- await clickBlockAppender();
- await page.keyboard.type( 'hello' );
-
- await clickBlockToolbarButton( 'Options' );
-
- // Change to HTML mode and close the options.
- await clickMenuItem( 'Edit as HTML' );
-
- // Focus on the textarea and enter an invalid paragraph
- await canvas().click(
- '.block-editor-block-list__layout .block-editor-block-list__block .block-editor-block-list__block-html-textarea'
- );
- await page.keyboard.type( 'invalid paragraph' );
-
- // Takes the focus away from the block so the invalid warning is triggered
- await page.click( '.editor-post-save-draft' );
-
- // Click on the 'three-dots' menu toggle.
- await canvas().click(
- '.block-editor-warning__actions button[aria-label="More options"]'
- );
-
- await clickMenuItem( 'Resolve' );
-
- // Check we get the resolve modal with the appropriate contents.
- const htmlBlockContent = await page.$eval(
- '.block-editor-block-compare__html',
- ( node ) => node.textContent
- );
- expect( htmlBlockContent ).toEqual(
- '
hello
invalid paragraph'
- );
- } );
-
- it( 'should strip potentially malicious on* attributes', async () => {
- let hasAlert = false;
-
- page.on( 'dialog', () => {
- hasAlert = true;
- } );
-
- // The paragraph block contains invalid HTML, which causes it to be an
- // invalid block.
- await setPostContent(
- `
-
-
aaaa 1
-
- `
- );
-
- // Give the browser time to show the alert.
- await page.evaluate( () => new Promise( window.requestIdleCallback ) );
-
- expect( console ).toHaveWarned();
- expect( console ).toHaveErrored();
- expect( hasAlert ).toBe( false );
- } );
-
- it( 'should not trigger malicious script tags when using a shortcode block', async () => {
- let hasAlert = false;
-
- page.on( 'dialog', () => {
- hasAlert = true;
- } );
-
- // The shortcode block contains invalid HTML, which causes it to be an
- // invalid block.
- await setPostContent(
- `
-
-
-
- `
- );
-
- // Give the browser time to show the alert.
- await page.evaluate( () => new Promise( window.requestIdleCallback ) );
- expect( hasAlert ).toBe( false );
- } );
-} );
diff --git a/packages/e2e-tests/specs/editor/various/nux.test.js b/packages/e2e-tests/specs/editor/various/nux.test.js
deleted file mode 100644
index f8b4e86628bfb3..00000000000000
--- a/packages/e2e-tests/specs/editor/various/nux.test.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- createNewPost,
- clickOnMoreMenuItem,
- canvas,
-} from '@wordpress/e2e-test-utils';
-
-describe( 'New User Experience (NUX)', () => {
- it( 'should show the guide to first-time users', async () => {
- let welcomeGuideText, welcomeGuide;
-
- // Create a new post as a first-time user.
- await createNewPost( { showWelcomeGuide: true } );
-
- // Guide should be on page 1 of 4
- welcomeGuideText = await page.$eval(
- '.edit-post-welcome-guide',
- ( element ) => element.innerText
- );
- expect( welcomeGuideText ).toContain( 'Welcome to the block editor' );
-
- // Click on the 'Next' button.
- const [ nextButton ] = await page.$x(
- '//button[contains(text(), "Next")]'
- );
- await nextButton.click();
-
- // Guide should be on page 2 of 4
- welcomeGuideText = await page.$eval(
- '.edit-post-welcome-guide',
- ( element ) => element.innerText
- );
- expect( welcomeGuideText ).toContain( 'Make each block your own' );
-
- // Click on the 'Previous' button.
- const [ previousButton ] = await page.$x(
- '//button[contains(text(), "Previous")]'
- );
- await previousButton.click();
-
- // Guide should be on page 1 of 4
- welcomeGuideText = await page.$eval(
- '.edit-post-welcome-guide',
- ( element ) => element.innerText
- );
- expect( welcomeGuideText ).toContain( 'Welcome to the block editor' );
-
- // Press the button for Page 2.
- await page.click( 'button[aria-label="Page 2 of 4"]' );
- await page.waitForXPath(
- '//h1[contains(text(), "Make each block your own")]'
- );
- // This shouldn't be necessary
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( 500 );
-
- // Press the right arrow key for Page 3.
- await page.keyboard.press( 'ArrowRight' );
- await page.waitForXPath(
- '//h1[contains(text(), "Get to know the block library")]'
- );
-
- // Press the right arrow key for Page 4.
- await page.keyboard.press( 'ArrowRight' );
- await page.waitForXPath(
- '//h1[contains(text(), "Learn how to use the block editor")]'
- );
-
- // Click on the *visible* 'Get started' button. There are two in the DOM
- // but only one is shown depending on viewport size.
- let getStartedButton;
- for ( const buttonHandle of await page.$x(
- '//button[contains(text(), "Get started")]'
- ) ) {
- if (
- await page.evaluate(
- ( button ) => button.style.display !== 'none',
- buttonHandle
- )
- ) {
- getStartedButton = buttonHandle;
- }
- }
- await getStartedButton.click();
-
- // Guide should be closed
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).toBeNull();
-
- // Reload the editor.
- await page.reload();
- await page.waitForSelector( '.edit-post-layout' );
-
- // Guide should be closed
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).toBeNull();
- } );
-
- it( 'should not show the welcome guide again if it is dismissed', async () => {
- let welcomeGuide;
-
- // Create a new post as a first-time user.
- await createNewPost( { showWelcomeGuide: true } );
-
- // Guide should be open
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).not.toBeNull();
-
- // Close the guide
- await page.click( '[role="dialog"] button[aria-label="Close"]' );
-
- // Reload the editor.
- await page.reload();
- await page.waitForSelector( '.edit-post-layout' );
-
- // Guide should be closed
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).toBeNull();
- } );
-
- it( 'should focus post title field after welcome guide is dismissed and post is empty', async () => {
- // Create a new post as a first-time user.
- await createNewPost( { showWelcomeGuide: true } );
-
- // Guide should be open.
- const welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).not.toBeNull();
-
- // Close the guide.
- await page.click( '[role="dialog"] button[aria-label="Close"]' );
-
- // Focus should be in post title field.
- const postTitle = await canvas().waitForSelector(
- 'h1[aria-label="Add title"'
- );
- await expect(
- postTitle.evaluate(
- ( node ) => node === node.ownerDocument.activeElement
- )
- ).resolves.toBe( true );
- } );
-
- it( 'should show the welcome guide if it is manually opened', async () => {
- let welcomeGuide;
-
- // Create a new post as a returning user.
- await createNewPost();
-
- // Guide should be closed
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).toBeNull();
-
- // Manually open the guide
- await clickOnMoreMenuItem( 'Welcome Guide' );
-
- // Guide should be open
- welcomeGuide = await page.$( '.edit-post-welcome-guide' );
- expect( welcomeGuide ).not.toBeNull();
- } );
-} );
diff --git a/packages/e2e-tests/specs/editor/various/publishing.test.js b/packages/e2e-tests/specs/editor/various/publishing.test.js
deleted file mode 100644
index fbac8cf98638bb..00000000000000
--- a/packages/e2e-tests/specs/editor/various/publishing.test.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- createNewPost,
- publishPost,
- publishPostWithPrePublishChecksDisabled,
- enablePrePublishChecks,
- disablePrePublishChecks,
- arePrePublishChecksEnabled,
- setBrowserViewport,
- openPublishPanel,
- pressKeyWithModifier,
- canvas,
-} from '@wordpress/e2e-test-utils';
-
-describe( 'Publishing', () => {
- describe.each( [ 'post', 'page' ] )(
- '%s locking prevent saving',
- ( postType ) => {
- beforeEach( async () => {
- await createNewPost( postType );
- } );
-
- it( `disables the publish button when a ${ postType } is locked`, async () => {
- await canvas().type(
- '.editor-post-title__input',
- 'E2E Test Post lock check publish button'
- );
- await page.evaluate( () =>
- wp.data
- .dispatch( 'core/editor' )
- .lockPostSaving( 'futurelock' )
- );
-
- await openPublishPanel();
-
- expect(
- await page.$(
- '.editor-post-publish-button[aria-disabled="true"]'
- )
- ).not.toBeNull();
- } );
-
- it( `disables the save shortcut when a ${ postType } is locked`, async () => {
- await canvas().type(
- '.editor-post-title__input',
- 'E2E Test Post check save shortcut'
- );
- await page.evaluate( () =>
- wp.data
- .dispatch( 'core/editor' )
- .lockPostSaving( 'futurelock' )
- );
- await pressKeyWithModifier( 'primary', 'S' );
-
- expect( await page.$( '.editor-post-saved-state' ) ).toBeNull();
- expect(
- await page.$( '.editor-post-save-draft' )
- ).not.toBeNull();
- } );
- }
- );
-
- describe.each( [ 'post', 'page' ] )( 'a %s', ( postType ) => {
- let werePrePublishChecksEnabled;
-
- beforeEach( async () => {
- await createNewPost( postType );
- werePrePublishChecksEnabled = await arePrePublishChecksEnabled();
- if ( ! werePrePublishChecksEnabled ) {
- await enablePrePublishChecks();
- }
- } );
-
- afterEach( async () => {
- if ( ! werePrePublishChecksEnabled ) {
- await disablePrePublishChecks();
- }
- } );
-
- it( `should publish the ${ postType } and close the panel once we start editing again.`, async () => {
- await canvas().type( '.editor-post-title__input', 'E2E Test Post' );
-
- await publishPost();
-
- // The post-publishing panel is visible.
- expect(
- await page.$( '.editor-post-publish-panel' )
- ).not.toBeNull();
-
- // Start editing again.
- await canvas().type( '.editor-post-title__input', ' (Updated)' );
-
- // The post-publishing panel is not visible anymore.
- expect( await page.$( '.editor-post-publish-panel' ) ).toBeNull();
- } );
- } );
-
- describe.each( [ 'post', 'page' ] )(
- 'a %s with pre-publish checks disabled',
- ( postType ) => {
- let werePrePublishChecksEnabled;
-
- beforeEach( async () => {
- await createNewPost( postType );
- werePrePublishChecksEnabled =
- await arePrePublishChecksEnabled();
- if ( werePrePublishChecksEnabled ) {
- await disablePrePublishChecks();
- }
- } );
-
- afterEach( async () => {
- if ( werePrePublishChecksEnabled ) {
- await enablePrePublishChecks();
- }
- } );
-
- it( `should publish the ${ postType } without opening the post-publish sidebar.`, async () => {
- await canvas().type(
- '.editor-post-title__input',
- 'E2E Test Post'
- );
-
- // The "Publish" button should be shown instead of the "Publish..." toggle.
- expect(
- await page.$( '.editor-post-publish-panel__toggle' )
- ).toBeNull();
- expect(
- await page.$( '.editor-post-publish-button' )
- ).not.toBeNull();
-
- await publishPostWithPrePublishChecksDisabled();
-
- // The post-publishing panel should have been not shown.
- expect(
- await page.$( '.editor-post-publish-panel' )
- ).toBeNull();
- } );
- }
- );
-
- describe.each( [ 'post', 'page' ] )(
- 'a %s in small viewports',
- ( postType ) => {
- let werePrePublishChecksEnabled;
-
- beforeEach( async () => {
- await createNewPost( postType );
- werePrePublishChecksEnabled =
- await arePrePublishChecksEnabled();
- if ( werePrePublishChecksEnabled ) {
- await disablePrePublishChecks();
- }
- await setBrowserViewport( 'small' );
- } );
-
- afterEach( async () => {
- await setBrowserViewport( 'large' );
- if ( werePrePublishChecksEnabled ) {
- await enablePrePublishChecks();
- }
- } );
-
- it( `should ignore the pre-publish checks and show the Publish... toggle instead of the Publish button`, async () => {
- expect(
- await page.$( '.editor-post-publish-panel__toggle' )
- ).not.toBeNull();
- expect(
- await page.$( '.editor-post-publish-button' )
- ).toBeNull();
- } );
- }
- );
-} );
diff --git a/packages/e2e-tests/specs/editor/various/scheduling.test.js b/packages/e2e-tests/specs/editor/various/scheduling.test.js
deleted file mode 100644
index df75dcb92f2820..00000000000000
--- a/packages/e2e-tests/specs/editor/various/scheduling.test.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { createNewPost, changeSiteTimezone } from '@wordpress/e2e-test-utils';
-
-async function getPublishButtonText() {
- return page.$eval(
- '.editor-post-publish-button__button',
- ( element ) => element.textContent
- );
-}
-
-describe( 'Scheduling', () => {
- const isDateTimeComponentFocused = () => {
- return page.evaluate( () => {
- const dateTimeElement = document.querySelector(
- '.components-datetime__date'
- );
- if ( ! dateTimeElement || ! document.activeElement ) {
- return false;
- }
- return dateTimeElement.contains( document.activeElement );
- } );
- };
-
- describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )(
- `Timezone %s`,
- ( timezone ) => {
- let oldTimezone;
- beforeEach( async () => {
- oldTimezone = await changeSiteTimezone( timezone );
- await createNewPost();
- } );
- afterEach( async () => {
- await changeSiteTimezone( oldTimezone );
- } );
-
- it( `should change publishing button text from "Publish" to "Schedule"`, async () => {
- expect( await getPublishButtonText() ).toBe( 'Publish' );
-
- // Open the datepicker.
- await page.click( '*[aria-label^="Change date"]' );
-
- // Change the publishing date to a year in the future.
- await page.click( '.components-datetime__time-field-year' );
- await page.keyboard.press( 'ArrowUp' );
-
- // Close the datepicker.
- await page.click( '.editor-post-schedule__dialog-toggle' );
-
- expect( await getPublishButtonText() ).toBe( 'Schedule…' );
- } );
- }
- );
-
- it( 'Should keep date time UI focused when the previous and next month buttons are clicked', async () => {
- await createNewPost();
-
- await page.click( '*[aria-label^="Change date"]' );
- await page.click( '*[aria-label="View previous month"]' );
- expect( await isDateTimeComponentFocused() ).toBe( true );
- await page.click( '*[aria-label="View next month"]' );
- expect( await isDateTimeComponentFocused() ).toBe( true );
- } );
-} );
diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js
index 11d07e52ec590d..f0a4dc762ac5c0 100644
--- a/packages/edit-post/src/components/header/writing-menu/index.js
+++ b/packages/edit-post/src/components/header/writing-menu/index.js
@@ -67,7 +67,7 @@ function WritingMenu() {
shortcut={ displayShortcut.primaryShift( '\\' ) }
/>
{
- toggle( 'core/edit-post', 'focusMode' );
+ toggle( 'core', 'focusMode' );
close();
},
} );
diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js
index 5ab081b78fcec8..5328048cc0742a 100644
--- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js
+++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js
@@ -94,7 +94,6 @@ export function useSpecificEditorSettings() {
const getPostLinkProps = usePostLinkProps();
const {
templateSlug,
- focusMode,
isDistractionFree,
hasFixedToolbar,
canvasMode,
@@ -121,7 +120,6 @@ export function useSpecificEditorSettings() {
const _context = getEditedPostContext();
return {
templateSlug: _record.slug,
- focusMode: !! getPreference( 'core/edit-site', 'focusMode' ),
isDistractionFree: !! getPreference(
'core/edit-site',
'distractionFree'
@@ -144,7 +142,7 @@ export function useSpecificEditorSettings() {
richEditingEnabled: true,
supportsTemplateMode: true,
- focusMode: canvasMode === 'view' && focusMode ? false : focusMode,
+ focusMode: canvasMode !== 'view',
isDistractionFree,
hasFixedToolbar,
defaultRenderingMode,
@@ -156,7 +154,6 @@ export function useSpecificEditorSettings() {
}, [
settings,
canvasMode,
- focusMode,
isDistractionFree,
hasFixedToolbar,
defaultRenderingMode,
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
index 08ff1a95d6e41a..062ad232e3ca98 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
@@ -21,7 +21,7 @@ import { search, closeSmall } from '@wordpress/icons';
/**
* Internal dependencies
*/
-import TabLayout from './tab-layout';
+import TabPanelLayout from './tab-panel-layout';
import { FontLibraryContext } from './context';
import FontsGrid from './fonts-grid';
import FontCard from './font-card';
@@ -156,7 +156,7 @@ function FontCollection( { id } ) {
};
return (
-
) }
-
+
);
}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js
index 1128ca0811977e..65a284560687cc 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js
@@ -2,7 +2,10 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Modal, TabPanel } from '@wordpress/components';
+import {
+ Modal,
+ privateApis as componentsPrivateApis,
+} from '@wordpress/components';
import { useContext } from '@wordpress/element';
/**
@@ -12,33 +15,33 @@ import InstalledFonts from './installed-fonts';
import FontCollection from './font-collection';
import UploadFonts from './upload-fonts';
import { FontLibraryContext } from './context';
+import { unlock } from '../../../lock-unlock';
+
+const { Tabs } = unlock( componentsPrivateApis );
const DEFAULT_TABS = [
{
- name: 'installed-fonts',
+ id: 'installed-fonts',
title: __( 'Library' ),
- className: 'installed-fonts',
},
{
- name: 'upload-fonts',
+ id: 'upload-fonts',
title: __( 'Upload' ),
- className: 'upload-fonts',
},
];
const tabsFromCollections = ( collections ) =>
collections.map( ( { id, name } ) => ( {
- name: id,
+ id,
title:
collections.length === 1 && id === 'default-font-collection'
? __( 'Install Fonts' )
: name,
- className: 'collection',
} ) );
function FontLibraryModal( {
onRequestClose,
- initialTabName = 'installed-fonts',
+ initialTabId = 'installed-fonts',
} ) {
const { collections } = useContext( FontLibraryContext );
@@ -54,22 +57,39 @@ function FontLibraryModal( {
isFullScreen
className="font-library-modal"
>
-
- { ( tab ) => {
- switch ( tab.name ) {
- case 'upload-fonts':
- return ;
- case 'installed-fonts':
- return ;
- default:
- return ;
- }
- } }
-
+
+
+
+ { tabs.map( ( { id, title } ) => (
+
+ { title }
+
+ ) ) }
+
+ { tabs.map( ( { id } ) => {
+ let contents;
+ switch ( id ) {
+ case 'upload-fonts':
+ contents = ;
+ break;
+ case 'installed-fonts':
+ contents = ;
+ break;
+ default:
+ contents = ;
+ }
+ return (
+
+ { contents }
+
+ );
+ } ) }
+
+
);
}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
index 2a8d1e591e084f..d493a2a297b18b 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
@@ -16,7 +16,7 @@ import {
/**
* Internal dependencies
*/
-import TabLayout from './tab-layout';
+import TabPanelLayout from './tab-panel-layout';
import { FontLibraryContext } from './context';
import FontsGrid from './fonts-grid';
import LibraryFontDetails from './library-font-details';
@@ -92,7 +92,7 @@ function InstalledFonts() {
}, [ notice ] );
return (
-
) }
-
+
);
}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
index 86cac4244dea93..cf7de98d6fbbb1 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
@@ -24,7 +24,7 @@
}
}
-.font-library-modal__tab-layout {
+.font-library-modal__tabpanel-layout {
main {
padding-bottom: 4rem;
@@ -75,7 +75,7 @@
padding-bottom: 1rem;
}
-.font-library-modal__tab-panel {
+.font-library-modal__tabs {
[role="tablist"] {
position: sticky;
top: 0;
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js
similarity index 85%
rename from packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js
rename to packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js
index 07f27cd31ea79c..a7151c6e908d61 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js
@@ -11,9 +11,15 @@ import {
} from '@wordpress/components';
import { chevronLeft } from '@wordpress/icons';
-function TabLayout( { title, description, handleBack, children, footer } ) {
+function TabPanelLayout( {
+ title,
+ description,
+ handleBack,
+ children,
+ footer,
+} ) {
return (
-
+
@@ -47,4 +53,4 @@ function TabLayout( { title, description, handleBack, children, footer } ) {
);
}
-export default TabLayout;
+export default TabPanelLayout;
diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js
index 4d892461a48043..4d56cf4670ab91 100644
--- a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js
@@ -100,7 +100,7 @@ export default function MoreMenu( { showIconLabels } ) {
) }
/>
field === 'sync-status'
+ )?.value;
const { patterns, isResolving } = usePatterns(
type,
isUncategorizedThemePatterns ? '' : categoryId,
{
search: view.search,
- // syncStatus:
- // deferredSyncedFilter === 'all'
- // ? undefined
- // : deferredSyncedFilter,
+ syncStatus: viewSyncStatus,
}
);
- const fields = useMemo(
- () => [
+ const fields = useMemo( () => {
+ const _fields = [
{
header: __( 'Preview' ),
id: 'preview',
render: ( { item } ) => (
),
- minWidth: 120,
- maxWidth: 120,
enableSorting: false,
enableHiding: false,
},
@@ -235,12 +253,36 @@ export default function DataviewsPatterns() {
render: ( { item } ) => (
),
- maxWidth: 400,
enableHiding: false,
},
- ],
- [ view.type, categoryId ]
- );
+ ];
+ if ( type === PATTERN_TYPES.theme ) {
+ _fields.push( {
+ header: __( 'Sync Status' ),
+ id: 'sync-status',
+ render: ( { item } ) => {
+ // User patterns can have their sync statuses checked directly.
+ // Non-user patterns are all unsynced for the time being.
+ return (
+ SYNC_FILTERS.find(
+ ( { value } ) => value === item.syncStatus
+ )?.label ||
+ SYNC_FILTERS.find(
+ ( { value } ) =>
+ value === PATTERN_SYNC_TYPES.unsynced
+ ).label
+ );
+ },
+ type: ENUMERATION_TYPE,
+ elements: SYNC_FILTERS,
+ filterBy: {
+ operators: [ OPERATOR_IN ],
+ },
+ enableSorting: false,
+ } );
+ }
+ return _fields;
+ }, [ view.type, categoryId, type ] );
// Reset the page number when the category changes.
useEffect( () => {
if ( previousCategoryId !== categoryId ) {
diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
index 118c954a851f3f..e82666902ed16a 100644
--- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
+++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
@@ -81,7 +81,7 @@ export default function DuplicateMenuItem( {
) }
{ isModalOpen && isTemplatePart && (
diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js
index bacb0f31908635..8d2cbaf7806b4d 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -114,8 +114,8 @@ function GridItem( { categoryId, item, ...props } ) {
const json = {
__file: item.type,
title: item.title || item.name,
- content: item.patternBlock.content.raw,
- syncStatus: item.patternBlock.wp_pattern_sync_status,
+ content: item.patternPost.content.raw,
+ syncStatus: item.patternPost.wp_pattern_sync_status,
};
return downloadBlob(
diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js
index be5992bd9b4efe..a0b82247c85a6d 100644
--- a/packages/edit-site/src/components/page-patterns/use-patterns.js
+++ b/packages/edit-site/src/components/page-patterns/use-patterns.js
@@ -184,29 +184,38 @@ const selectPatterns = createSelector(
]
);
-const patternBlockToPattern = ( patternBlock, categories ) => ( {
- blocks: parse( patternBlock.content.raw, {
+/**
+ * Converts a post of type `wp_block` to a 'pattern item' that more closely
+ * matches the structure of theme provided patterns.
+ *
+ * @param {Object} patternPost The `wp_block` record being normalized.
+ * @param {Map} categories A Map of user created categories.
+ *
+ * @return {Object} The normalized item.
+ */
+const convertPatternPostToItem = ( patternPost, categories ) => ( {
+ blocks: parse( patternPost.content.raw, {
__unstableSkipMigrationLogs: true,
} ),
- ...( patternBlock.wp_pattern_category.length > 0 && {
- categories: patternBlock.wp_pattern_category.map(
+ ...( patternPost.wp_pattern_category.length > 0 && {
+ categories: patternPost.wp_pattern_category.map(
( patternCategoryId ) =>
categories && categories.get( patternCategoryId )
? categories.get( patternCategoryId ).slug
: patternCategoryId
),
} ),
- termLabels: patternBlock.wp_pattern_category.map( ( patternCategoryId ) =>
+ termLabels: patternPost.wp_pattern_category.map( ( patternCategoryId ) =>
categories?.get( patternCategoryId )
? categories.get( patternCategoryId ).label
: patternCategoryId
),
- id: patternBlock.id,
- name: patternBlock.slug,
- syncStatus: patternBlock.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full,
- title: patternBlock.title.raw,
- type: PATTERN_TYPES.user,
- patternBlock,
+ id: patternPost.id,
+ name: patternPost.slug,
+ syncStatus: patternPost.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full,
+ title: patternPost.title.raw,
+ type: patternPost.type,
+ patternPost,
} );
const selectUserPatterns = createSelector(
@@ -215,7 +224,7 @@ const selectUserPatterns = createSelector(
select( coreStore );
const query = { per_page: -1 };
- const records = getEntityRecords(
+ const patternPosts = getEntityRecords(
'postType',
PATTERN_TYPES.user,
query
@@ -225,9 +234,9 @@ const selectUserPatterns = createSelector(
userPatternCategories.forEach( ( userCategory ) =>
categories.set( userCategory.id, userCategory )
);
- let patterns = records
- ? records.map( ( record ) =>
- patternBlockToPattern( record, categories )
+ let patterns = patternPosts
+ ? patternPosts.map( ( record ) =>
+ convertPatternPostToItem( record, categories )
)
: EMPTY_PATTERN_LIST;
diff --git a/packages/edit-site/src/components/preferences-modal/index.js b/packages/edit-site/src/components/preferences-modal/index.js
index f83acb1af5ca90..8f6a8a5794cb9e 100644
--- a/packages/edit-site/src/components/preferences-modal/index.js
+++ b/packages/edit-site/src/components/preferences-modal/index.js
@@ -107,6 +107,7 @@ export default function EditSitePreferencesModal() {
label={ __( 'Distraction free' ) }
/>
{
- toggle( 'core/edit-site', 'focusMode' );
+ toggle( 'core', 'focusMode' );
close();
},
} );
diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js
index 50b95976f778e6..58d5c84f15575e 100644
--- a/packages/edit-site/src/index.js
+++ b/packages/edit-site/src/index.js
@@ -54,7 +54,6 @@ export function initializeEditor( id, settings ) {
dispatch( preferencesStore ).setDefaults( 'core/edit-site', {
editorMode: 'visual',
fixedToolbar: false,
- focusMode: false,
distractionFree: false,
welcomeGuide: true,
welcomeGuideStyles: true,
@@ -64,6 +63,7 @@ export function initializeEditor( id, settings ) {
dispatch( preferencesStore ).setDefaults( 'core', {
allowRightClickOverrides: true,
+ focusMode: false,
keepCaretInsideBlock: false,
showBlockBreadcrumbs: true,
showListViewByDefault: false,
diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js
index 5ab4ef3edb1b17..e28ba9692dc289 100644
--- a/packages/editor/src/components/provider/use-block-editor-settings.js
+++ b/packages/editor/src/components/provider/use-block-editor-settings.js
@@ -46,11 +46,11 @@ const BLOCK_EDITOR_SETTINGS = [
'enableCustomSpacing',
'enableCustomUnits',
'enableOpenverseMediaCategory',
- 'focusMode',
'distractionFree',
'fontSizes',
'gradients',
'generateAnchors',
+ 'getPostLinkProps',
'hasFixedToolbar',
'hasInlineToolbar',
'isDistractionFree',
@@ -75,7 +75,6 @@ const BLOCK_EDITOR_SETTINGS = [
'__unstableIsBlockBasedTheme',
'__experimentalArchiveTitleTypeLabel',
'__experimentalArchiveTitleNameLabel',
- '__experimentalGetPostLinkProps',
];
/**
@@ -90,6 +89,7 @@ const BLOCK_EDITOR_SETTINGS = [
function useBlockEditorSettings( settings, postType, postId ) {
const {
allowRightClickOverrides,
+ focusMode,
keepCaretInsideBlock,
reusableBlocks,
hasUploadPermissions,
@@ -100,7 +100,6 @@ function useBlockEditorSettings( settings, postType, postId ) {
userPatternCategories,
restBlockPatterns,
restBlockPatternCategories,
- getPostLinkProps,
} = useSelect(
( select ) => {
const isWeb = Platform.OS === 'web';
@@ -113,8 +112,6 @@ function useBlockEditorSettings( settings, postType, postId ) {
getBlockPatterns,
getBlockPatternCategories,
} = select( coreStore );
- const { getPostLinkProps: postLinkProps } =
- select( editorStore ).getEditorSettings();
const { get } = select( preferencesStore );
const siteSettings = canUser( 'read', 'settings' )
@@ -131,6 +128,7 @@ function useBlockEditorSettings( settings, postType, postId ) {
postType,
postId
)?._links?.hasOwnProperty( 'wp:action-unfiltered-html' ),
+ focusMode: get( 'core', 'focusMode' ),
keepCaretInsideBlock: get( 'core', 'keepCaretInsideBlock' ),
reusableBlocks: isWeb
? getEntityRecords( 'postType', 'wp_block', {
@@ -144,7 +142,6 @@ function useBlockEditorSettings( settings, postType, postId ) {
userPatternCategories: getUserPatternCategories(),
restBlockPatterns: getBlockPatterns(),
restBlockPatternCategories: getBlockPatternCategories(),
- getPostLinkProps: postLinkProps,
};
},
[ postType, postId ]
@@ -214,6 +211,8 @@ function useBlockEditorSettings( settings, postType, postId ) {
[ saveEntityRecord, userCanCreatePages ]
);
+ const forceDisableFocusMode = settings.focusMode === false;
+
return useMemo(
() => ( {
...Object.fromEntries(
@@ -222,6 +221,7 @@ function useBlockEditorSettings( settings, postType, postId ) {
)
),
allowRightClickOverrides,
+ focusMode: focusMode && ! forceDisableFocusMode,
keepCaretInsideBlock,
mediaUpload: hasUploadPermissions ? mediaUpload : undefined,
__experimentalReusableBlocks: reusableBlocks,
@@ -253,10 +253,11 @@ function useBlockEditorSettings( settings, postType, postId ) {
? [ [ 'core/navigation', {}, [] ] ]
: settings.template,
__experimentalSetIsInserterOpened: setIsInserterOpened,
- __experimentalGetPostLinkProps: getPostLinkProps,
} ),
[
allowRightClickOverrides,
+ focusMode,
+ forceDisableFocusMode,
keepCaretInsideBlock,
settings,
hasUploadPermissions,
@@ -272,7 +273,6 @@ function useBlockEditorSettings( settings, postType, postId ) {
pageForPosts,
postType,
setIsInserterOpened,
- getPostLinkProps,
]
);
}
diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js
index 693375af3d79c5..ef522721787d7e 100644
--- a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js
+++ b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js
@@ -6,6 +6,7 @@ export default function convertEditorSettings( data ) {
let newData = data;
const settingsToMoveToCore = [
'allowRightClickOverrides',
+ 'focusMode',
'keepCaretInsideBlock',
'showBlockBreadcrumbs',
'showIconLabels',
diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java
index 71df8e0c2888a2..380cdd1c5d6132 100644
--- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java
+++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java
@@ -1,5 +1,7 @@
package org.wordpress.mobile.ReactNativeAztec;
+import static android.content.ClipData.Item;
+
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -10,18 +12,19 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
+import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.ThemedReactContext;
@@ -41,12 +44,10 @@
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
-import java.util.HashSet;
-import java.util.HashMap;
-
-import static android.content.ClipData.*;
public class ReactAztecText extends AztecText {
@@ -64,6 +65,7 @@ public class ReactAztecText extends AztecText {
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
private @Nullable ContentSizeWatcher mContentSizeWatcher;
private @Nullable ScrollWatcher mScrollWatcher;
+ private @Nullable Runnable mKeyboardRunnable;
// FIXME: Used in `incrementAndGetEventCounter` but never read. I guess we can get rid of it, but before this
// check when it's used in EditText in RN. (maybe tests?)
@@ -264,18 +266,46 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
}
private void showSoftKeyboard() {
- new Handler(Looper.getMainLooper()).post(new Runnable() {
+ // If the text input is already focused we can show the keyboard.
+ if(hasWindowFocus()) {
+ showSoftKeyboardNow();
+ }
+ // Otherwise, we'll wait until it gets focused.
+ else {
+ getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ if (hasFocus) {
+ showSoftKeyboardNow();
+ getViewTreeObserver().removeOnWindowFocusChangeListener(this);
+ }
+ }
+ });
+ }
+ }
+
+ private void showSoftKeyboardNow() {
+ // Cancel any previously scheduled Runnable
+ if (mKeyboardRunnable != null) {
+ removeCallbacks(mKeyboardRunnable);
+ }
+
+ mKeyboardRunnable = new Runnable() {
@Override
public void run() {
if (mInputMethodManager != null) {
- mInputMethodManager.showSoftInput(ReactAztecText.this, 0);
+ mInputMethodManager.showSoftInput(ReactAztecText.this, InputMethodManager.SHOW_IMPLICIT);
}
}
- });
+ };
+
+ post(mKeyboardRunnable);
}
private void hideSoftKeyboard() {
- mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ if (mInputMethodManager != null) {
+ mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
}
public void setScrollWatcher(ScrollWatcher scrollWatcher) {
diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json
index 4b24bdbc707562..f91b214758b49a 100644
--- a/packages/react-native-aztec/package.json
+++ b/packages/react-native-aztec/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-aztec",
- "version": "1.109.3",
+ "version": "1.110.0",
"description": "Aztec view for react-native.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
index ce427be2ad09b0..ec847d71bf51c9 100644
--- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
+++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
@@ -92,7 +92,6 @@ data class GutenbergProps @JvmOverloads constructor(
content?.let { putString(PROP_INITIAL_DATA, it) }
}
- private const val PROP_INITIAL_TITLE = "initialTitle"
private const val PROP_INITIAL_HTML_MODE_ENABLED = "initialHtmlModeEnabled"
private const val PROP_POST_TYPE = "postType"
private const val PROP_HOST_APP_NAMESPACE = "hostAppNamespace"
@@ -105,6 +104,7 @@ data class GutenbergProps @JvmOverloads constructor(
private const val PROP_QUOTE_BLOCK_V2 = "quoteBlockV2"
private const val PROP_LIST_BLOCK_V2 = "listBlockV2"
+ const val PROP_INITIAL_TITLE = "initialTitle"
const val PROP_INITIAL_DATA = "initialData"
const val PROP_STYLES = "rawStyles"
const val PROP_FEATURES = "rawFeatures"
diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json
index 586c5159ef165f..276e536dbf929f 100644
--- a/packages/react-native-bridge/package.json
+++ b/packages/react-native-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-bridge",
- "version": "1.109.3",
+ "version": "1.110.0",
"description": "Native bridge library used to integrate the block editor into a native App.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 23ee8d50a2e73b..ed3f7b5e961eb6 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+
+## 1.110.0
- [*] [internal] Move InserterButton from components package to block-editor package [#56494]
- [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775]
- [*] Fix crash when blockType wrapperProps are not defined [#56846]
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
index e7ee4a20df03f2..230c844491d282 100644
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
+++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
@@ -27,6 +27,10 @@ describe( 'Gutenberg Editor Rotation tests', () => {
await editorPage.addNewBlock( blockNames.paragraph );
if ( isAndroid() ) {
+ // Waits until the keyboard is visible
+ await editorPage.driver.waitUntil(
+ editorPage.driver.isKeyboardShown
+ );
await editorPage.dismissKeyboard();
}
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js
deleted file mode 100644
index 50a2a3ee8fd640..00000000000000
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Internal dependencies
- */
-import { blockNames } from './pages/editor-page';
-import testData from './helpers/test-data';
-
-describe( 'Gutenberg Editor tests', () => {
- it( 'should be able to create a post with heading and paragraph blocks', async () => {
- await editorPage.initializeEditor();
- await editorPage.addNewBlock( blockNames.heading );
- let headingBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.heading
- );
-
- await editorPage.typeTextToTextBlock(
- headingBlockElement,
- testData.heading
- );
-
- await editorPage.addNewBlock( blockNames.paragraph );
- let paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 2
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- testData.mediumText
- );
-
- await editorPage.addNewBlock( blockNames.paragraph );
- paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 3
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- testData.mediumText
- );
-
- await editorPage.addNewBlock( blockNames.heading );
- headingBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.heading,
- 4
- );
- await editorPage.typeTextToTextBlock(
- headingBlockElement,
- testData.heading
- );
-
- await editorPage.addNewBlock( blockNames.paragraph );
- paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 5
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- testData.mediumText
- );
-
- // Assert that even though there are 5 blocks, there should only be 3 paragraph blocks
- expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 );
- } );
-} );
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js
deleted file mode 100644
index 8f21ef04858fb6..00000000000000
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * Internal dependencies
- */
-import { blockNames } from './pages/editor-page';
-import {
- backspace,
- clickMiddleOfElement,
- clickBeginningOfElement,
-} from './helpers/utils';
-import testData from './helpers/test-data';
-
-describe( 'Gutenberg Editor tests for Paragraph Block', () => {
- it( 'should be able to split one paragraph block into two', async () => {
- await editorPage.initializeEditor();
- await editorPage.addNewBlock( blockNames.paragraph );
- const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- testData.shortText
- );
- await clickMiddleOfElement( editorPage.driver, paragraphBlockElement );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- '\n',
- false
- );
- const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 );
- const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 );
- expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 );
- expect( text0 ).not.toBe( '' );
- expect( text1 ).not.toBe( '' );
- expect( testData.shortText ).toMatch(
- new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` )
- );
- } );
-
- it( 'should be able to merge 2 paragraph blocks into 1', async () => {
- await editorPage.initializeEditor();
- await editorPage.addNewBlock( blockNames.paragraph );
- let paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph
- );
-
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- testData.shortText
- );
- await clickMiddleOfElement( editorPage.driver, paragraphBlockElement );
- await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' );
-
- const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 );
- const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 );
- expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 );
- paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 2
- );
-
- await clickBeginningOfElement(
- editorPage.driver,
- paragraphBlockElement
- );
-
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- backspace
- );
-
- const text = await editorPage.getTextForParagraphBlockAtPosition( 1 );
- expect( text0 + text1 ).toMatch( text );
- paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 1
- );
- await paragraphBlockElement.click();
- expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 1 );
- } );
-
- it( 'should be able to create a post with multiple paragraph blocks', async () => {
- await editorPage.initializeEditor();
- await editorPage.addNewBlock( blockNames.paragraph );
- await editorPage.sendTextToParagraphBlock( 1, testData.longText );
- expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 );
- } );
-
- it( 'should be able to merge blocks with unknown html elements', async () => {
- await editorPage.initializeEditor( {
- initialData: [
- testData.unknownElementParagraphBlock,
- testData.lettersInParagraphBlock,
- ].join( '\n\n' ),
- } );
-
- // Merge paragraphs.
- const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 2
- );
-
- const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 );
- const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 );
-
- await clickBeginningOfElement(
- editorPage.driver,
- paragraphBlockElement
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- backspace
- );
-
- // Verify the editor has not crashed.
- const mergedBlockText =
- await editorPage.getTextForParagraphBlockAtPosition( 1 );
- expect( text0 + text1 ).toMatch( mergedBlockText );
- } );
-
- // Based on https://github.com/wordpress-mobile/gutenberg-mobile/pull/1507
- it( 'should handle multiline paragraphs from web', async () => {
- await editorPage.initializeEditor( {
- initialData: [
- testData.multiLinesParagraphBlock,
- testData.paragraphBlockEmpty,
- ].join( '\n\n' ),
- } );
-
- // Merge paragraphs.
- const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
- blockNames.paragraph,
- 2
- );
- await editorPage.typeTextToTextBlock(
- paragraphBlockElement,
- backspace
- );
-
- // Verify the editor has not crashed.
- const text = await editorPage.getTextForParagraphBlockAtPosition( 1 );
- expect( text.length ).not.toEqual( 0 );
- } );
-} );
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js
new file mode 100644
index 00000000000000..3a12bf5d13345b
--- /dev/null
+++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js
@@ -0,0 +1,333 @@
+/**
+ * Internal dependencies
+ */
+import { blockNames } from './pages/editor-page';
+import {
+ backspace,
+ clickBeginningOfElement,
+ waitForMediaLibrary,
+} from './helpers/utils';
+import testData from './helpers/test-data';
+
+describe( 'Gutenberg Editor Writing flow tests', () => {
+ it( 'should be able to write a post title', async () => {
+ await editorPage.initializeEditor( { initialTitle: '' } );
+
+ const titleInput = await editorPage.getEmptyTitleTextInputElement();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+ await editorPage.typeTextToTextBlock( titleInput, testData.shortText );
+
+ // Trigger the return key to go to the first Paragraph
+ await editorPage.typeTextToTextBlock( titleInput, '\n' );
+
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph
+ );
+ expect( paragraphBlockElement ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ // Trigger the return key to delete the Paragraph block
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ backspace
+ );
+ // Expect to have an empty Paragraph block and the keyboard visible
+ expect(
+ await editorPage.getTextBlockAtPosition( blockNames.paragraph )
+ ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+ } );
+
+ it( 'should be able to create a new Paragraph block when pressing the enter key', async () => {
+ await editorPage.initializeEditor( { initialTitle: '' } );
+
+ const defaultBlockAppenderElement =
+ await editorPage.getDefaultBlockAppenderElement();
+ await defaultBlockAppenderElement.click();
+
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph
+ );
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ testData.shortText
+ );
+ await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' );
+
+ // Expect to have a new Paragraph block and the keyboard visible
+ expect(
+ await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 )
+ ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+ } );
+
+ it( 'should automatically dismiss the keyboard when selecting non-text-based-blocks', async () => {
+ await editorPage.initializeEditor( { initialTitle: '' } );
+
+ await editorPage.addNewBlock( blockNames.image );
+ // Wait for the Media picker to show up
+ await waitForMediaLibrary( editorPage.driver );
+
+ // Select the WordPress Media Library option
+ await editorPage.chooseMediaLibrary();
+
+ // Wait until the media is added
+ await editorPage.driver.pause( 500 );
+
+ const captionElement = await editorPage.getImageBlockCaptionButton();
+ await captionElement.click();
+ const captionInput =
+ await editorPage.getImageBlockCaptionInput( captionElement );
+
+ expect( captionInput ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ // Sets a new caption
+ await editorPage.typeTextToTextBlock(
+ captionInput,
+ testData.listItem2,
+ true
+ );
+
+ // Trigger the return key to exit the caption and create a new Paragraph block
+ await editorPage.typeTextToTextBlock( captionInput, '\n' );
+
+ // Expect to have an empty Paragraph block and the keyboard visible
+ let paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 2
+ );
+ expect( paragraphBlockElement ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ backspace
+ );
+
+ // When deleting the Paragraph block, the keyboard should be hidden and
+ // the image block should be focused.
+ await editorPage.driver.waitUntil( async function () {
+ return ! ( await editorPage.driver.isKeyboardShown() );
+ } );
+ expect( await editorPage.isImageBlockSelected() ).toBe( true );
+
+ // Adding a new Paragraph block
+ await editorPage.addNewBlock( blockNames.paragraph );
+ paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 2
+ );
+
+ // It should be focused and the keyboard should be visible
+ expect( paragraphBlockElement ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ testData.shortText
+ );
+
+ const imageBlockElement = await editorPage.getBlockAtPosition(
+ blockNames.image
+ );
+ await imageBlockElement.click();
+
+ await editorPage.driver.waitUntil( async function () {
+ return ! ( await editorPage.driver.isKeyboardShown() );
+ } );
+ expect( await editorPage.isImageBlockSelected() ).toBe( true );
+ } );
+
+ it( 'should manually dismiss the keyboard', async () => {
+ await editorPage.initializeEditor( { initialTitle: '' } );
+
+ const defaultBlockAppenderElement =
+ await editorPage.getDefaultBlockAppenderElement();
+ await defaultBlockAppenderElement.click();
+
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph
+ );
+ expect( paragraphBlockElement ).toBeTruthy();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ await editorPage.dismissKeyboard();
+
+ // Checks that no block is selected by looking for the block menu actions button
+ expect( await editorPage.isBlockActionsMenuButtonDisplayed() ).toBe(
+ false
+ );
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( false );
+ } );
+
+ it( 'should dismiss the keyboard and show it back when opening modals', async () => {
+ await editorPage.initializeEditor( { initialTitle: '' } );
+
+ const defaultBlockAppenderElement =
+ await editorPage.getDefaultBlockAppenderElement();
+ await defaultBlockAppenderElement.click();
+
+ await editorPage.openBlockSettings();
+ await editorPage.driver.waitUntil( async function () {
+ return ! ( await editorPage.driver.isKeyboardShown() );
+ } );
+
+ await editorPage.dismissBottomSheet();
+
+ await editorPage.driver.waitUntil( editorPage.driver.isKeyboardShown );
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph
+ );
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ testData.listItem1
+ );
+ const typedText = await paragraphBlockElement.getText();
+ expect( typedText ).toMatch( testData.listItem1 );
+ } );
+
+ it( 'should be able to split and merge paragraph blocks', async () => {
+ await editorPage.initializeEditor();
+
+ // Add the first Paragraph block using the default block appender
+ const defaultBlockAppenderElement =
+ await editorPage.getDefaultBlockAppenderElement();
+ await defaultBlockAppenderElement.click();
+
+ // Type text into the first Paragraph block
+ const firstParagraphBlockElement =
+ await editorPage.getTextBlockAtPosition( blockNames.paragraph );
+ await editorPage.typeTextToTextBlock(
+ firstParagraphBlockElement,
+ testData.shortText
+ );
+
+ // Add a second Paragraph block and type some text
+ await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock();
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+ const secondParagraphBlockElement =
+ await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 );
+ await editorPage.typeTextToTextBlock(
+ secondParagraphBlockElement,
+ testData.mediumText
+ );
+
+ // Merge Paragraph blocks
+ await clickBeginningOfElement(
+ editorPage.driver,
+ secondParagraphBlockElement
+ );
+ await editorPage.typeTextToTextBlock(
+ secondParagraphBlockElement,
+ backspace
+ );
+
+ // Wait for blocks to be merged
+ await editorPage.driver.waitUntil( async function () {
+ return ( await editorPage.getNumberOfParagraphBlocks() ) === 1;
+ } );
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ // Split the current Paragraph block right where the caret is positioned
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 1,
+ true
+ );
+ await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' );
+
+ // Wait for blocks to be split
+ await editorPage.driver.waitUntil( async function () {
+ return ( await editorPage.getNumberOfParagraphBlocks() ) === 2;
+ } );
+ expect( await editorPage.driver.isKeyboardShown() ).toBe( true );
+
+ const firstParagraphText =
+ await editorPage.getTextForParagraphBlockAtPosition( 1 );
+ const secondParagraphText =
+ await editorPage.getTextForParagraphBlockAtPosition( 2 );
+
+ expect( firstParagraphText ).toEqual( testData.shortText );
+ expect( secondParagraphText ).toEqual( testData.mediumText );
+ } );
+
+ it( 'should be able to create a post with multiple paragraph blocks', async () => {
+ await editorPage.initializeEditor();
+ const defaultBlockAppenderElement =
+ await editorPage.getDefaultBlockAppenderElement();
+ await defaultBlockAppenderElement.click();
+
+ await editorPage.sendTextToParagraphBlock( 1, testData.longText );
+ expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 );
+ } );
+
+ it( 'should be able to merge blocks with unknown html elements', async () => {
+ await editorPage.initializeEditor( {
+ initialData: [
+ testData.unknownElementParagraphBlock,
+ testData.lettersInParagraphBlock,
+ ].join( '\n\n' ),
+ } );
+
+ // Merge paragraphs.
+ const paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 2
+ );
+
+ const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 );
+ const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 );
+
+ await clickBeginningOfElement(
+ editorPage.driver,
+ paragraphBlockElement
+ );
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ backspace
+ );
+
+ // Verify the editor has not crashed.
+ const mergedBlockText =
+ await editorPage.getTextForParagraphBlockAtPosition( 1 );
+ expect( text0 + text1 ).toMatch( mergedBlockText );
+ } );
+
+ it( 'should be able to create a post with heading and paragraph blocks', async () => {
+ await editorPage.initializeEditor();
+ await editorPage.addNewBlock( blockNames.heading );
+ const headingBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.heading
+ );
+
+ await editorPage.typeTextToTextBlock(
+ headingBlockElement,
+ testData.heading
+ );
+
+ await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock();
+ let paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 2
+ );
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ testData.mediumText
+ );
+
+ await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock();
+ paragraphBlockElement = await editorPage.getTextBlockAtPosition(
+ blockNames.paragraph,
+ 3
+ );
+ await editorPage.typeTextToTextBlock(
+ paragraphBlockElement,
+ testData.mediumText
+ );
+
+ // Assert that even though there are 3 blocks, there should only be 2 paragraph blocks
+ expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 );
+ } );
+} );
diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js
index a19aaf5445d79f..b00be20458e802 100644
--- a/packages/react-native-editor/__device-tests__/pages/editor-page.js
+++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js
@@ -45,8 +45,18 @@ class EditorPage {
}
}
- async initializeEditor( { initialData, rawStyles, rawFeatures } = {} ) {
- await launchApp( this.driver, { initialData, rawStyles, rawFeatures } );
+ async initializeEditor( {
+ initialTitle,
+ initialData,
+ rawStyles,
+ rawFeatures,
+ } = {} ) {
+ await launchApp( this.driver, {
+ initialTitle,
+ initialData,
+ rawStyles,
+ rawFeatures,
+ } );
// Stores initial values from the editor for different helpers.
const addButton = await this.driver.$$( `~${ ADD_BLOCK_ID }` );
@@ -72,9 +82,13 @@ class EditorPage {
// Text blocks functions
// E.g. Paragraph, Heading blocks
// ===============================
- async getTextBlockAtPosition( blockName, position = 1 ) {
+ async getTextBlockAtPosition(
+ blockName,
+ position = 1,
+ skipWrapperClick = false
+ ) {
// iOS needs a click to get the text element
- if ( ! isAndroid() ) {
+ if ( ! isAndroid() && ! skipWrapperClick ) {
const textBlockLocator = `(//XCUIElementTypeButton[contains(@name, "${ blockName } Block. Row ${ position }")])`;
await clickIfClickable( this.driver, textBlockLocator );
@@ -171,14 +185,25 @@ class EditorPage {
}
async addParagraphBlockByTappingEmptyAreaBelowLastBlock() {
- const emptyAreaBelowLastBlock =
- await this.driver.elementByAccessibilityId( 'Add paragraph block' );
+ const element = isAndroid()
+ ? '~Add paragraph block'
+ : '(//XCUIElementTypeOther[@name="Add paragraph block"])';
+ const emptyAreaBelowLastBlock = await this.driver.$( element );
await emptyAreaBelowLastBlock.click();
}
- async getTitleElement( options = { autoscroll: false } ) {
+ async getDefaultBlockAppenderElement() {
+ const appenderElement = isAndroid()
+ ? `//android.widget.EditText[@text='Start writing…']`
+ : '(//XCUIElementTypeOther[contains(@name, "Start writing…")])[2]';
+ return this.driver.$( appenderElement );
+ }
+
+ async getTitleElement( options = { autoscroll: false, isEmpty: false } ) {
const titleElement = isAndroid()
- ? 'Post title. Welcome to Gutenberg!'
+ ? `Post title. ${
+ options.isEmpty ? 'Empty' : 'Welcome to Gutenberg!'
+ }`
: 'post-title';
if ( options.autoscroll ) {
@@ -200,6 +225,18 @@ class EditorPage {
return elements[ 0 ];
}
+ async getEmptyTitleTextInputElement() {
+ const titleWrapperElement = await this.getTitleElement( {
+ isEmpty: true,
+ } );
+ await titleWrapperElement.click();
+
+ const titleElement = isAndroid()
+ ? '//android.widget.EditText[@content-desc="Post title. Empty"]'
+ : '~Add title';
+ return this.driver.$( titleElement );
+ }
+
// iOS loads the block list more eagerly compared to Android.
// This makes this function return elements without scrolling on iOS.
// So we are keeping this Android only.
@@ -370,10 +407,14 @@ class EditorPage {
await settingsButton.click();
}
- async removeBlock() {
- const blockActionsButtonElement = isAndroid()
+ getBlockActionsMenuElement() {
+ return isAndroid()
? '//android.widget.Button[contains(@content-desc, "Open Block Actions Menu")]'
: '//XCUIElementTypeButton[@name="Open Block Actions Menu"]';
+ }
+
+ async removeBlock() {
+ const blockActionsButtonElement = this.getBlockActionsMenuElement();
const blockActionsMenu = await this.swipeToolbarToElement(
blockActionsButtonElement
);
@@ -391,6 +432,12 @@ class EditorPage {
return await swipeDown( this.driver );
}
+ async isBlockActionsMenuButtonDisplayed() {
+ const menuButtonElement = this.getBlockActionsMenuElement();
+ const elementsFound = await this.driver.$$( menuButtonElement );
+ return elementsFound.length !== 0;
+ }
+
// =========================
// Block toolbar functions
// =========================
@@ -406,8 +453,6 @@ class EditorPage {
swipeRight: true,
} );
await addButton[ 0 ].click();
- // Wait for Bottom sheet animation to finish
- await this.driver.pause( 3000 );
}
// Click on block of choice.
@@ -425,10 +470,9 @@ class EditorPage {
const inserterElement = isAndroid()
? 'Blocks menu'
: 'InserterUI-Blocks';
- return await this.waitForElementToBeDisplayedById(
- inserterElement,
- 4000
- );
+ await this.driver
+ .$( `~${ inserterElement }` )
+ .waitForDisplayed( { timeout: 4000 } );
}
static async isElementOutOfBounds( element, { width, height } = {} ) {
@@ -787,13 +831,25 @@ class EditorPage {
await clickIfClickable( this.driver, mediaLibraryLocator );
}
+ async getImageBlockCaptionButton() {
+ const captionElement = isAndroid()
+ ? '//android.widget.Button[starts-with(@content-desc, "Image caption")]'
+ : '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]';
+ return this.driver.$( captionElement );
+ }
+
+ async getImageBlockCaptionInput( imageBlockCaptionButton ) {
+ const captionInputElement = isAndroid()
+ ? '//android.widget.EditText'
+ : '//XCUIElementTypeTextView';
+ return imageBlockCaptionButton.$( captionInputElement );
+ }
+
async enterCaptionToSelectedImageBlock( caption, clear = true ) {
- const imageBlockCaptionButton = await this.driver.$(
- '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]'
- );
+ const imageBlockCaptionButton = await this.getImageBlockCaptionButton();
await imageBlockCaptionButton.click();
- const imageBlockCaptionField = await imageBlockCaptionButton.$(
- '//XCUIElementTypeTextView'
+ const imageBlockCaptionField = await this.getImageBlockCaptionInput(
+ imageBlockCaptionButton
);
await typeString( this.driver, imageBlockCaptionField, caption, clear );
}
@@ -814,6 +870,16 @@ class EditorPage {
.perform();
}
+ async isImageBlockSelected() {
+ // Since there isn't an easy way to see if a block is selected,
+ // it will check if the edit image button is visible
+ const editImageElement = isAndroid()
+ ? '(//android.widget.Button[@content-desc="Edit image"])'
+ : '(//XCUIElementTypeButton[@name="Edit image"])';
+
+ return await this.driver.$( editImageElement ).isDisplayed();
+ }
+
// =============================
// Search Block functions
// =============================
diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
index 69985317ddad33..3ea19fa97b3831 100644
--- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
+++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
@@ -113,6 +113,8 @@ protected void onCreate(Bundle savedInstanceState) {
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ linearLayout.setFocusable(false);
+ linearLayout.setFocusableInTouchMode(true);
// Create a Toolbar instance
Toolbar toolbar = new Toolbar(this);
@@ -166,6 +168,7 @@ private Bundle getAppOptions() {
Bundle bundle = new Bundle();
// Parse initial props from launch arguments
+ String initialTitle = null;
String initialData = null;
String rawStyles = null;
String rawFeatures = null;
@@ -175,6 +178,9 @@ private Bundle getAppOptions() {
String initialProps = extrasBundle.getString(EXTRAS_INITIAL_PROPS, "{}");
try {
JSONObject jsonObject = new JSONObject(initialProps);
+ if (jsonObject.has(GutenbergProps.PROP_INITIAL_TITLE)) {
+ initialTitle = jsonObject.getString(GutenbergProps.PROP_INITIAL_TITLE);
+ }
if (jsonObject.has(GutenbergProps.PROP_INITIAL_DATA)) {
initialData = jsonObject.getString(GutenbergProps.PROP_INITIAL_DATA);
}
@@ -209,6 +215,9 @@ private Bundle getAppOptions() {
capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_SMARTFRAME_EMBED_BLOCK, true);
bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities);
+ if(initialTitle != null) {
+ bundle.putString(GutenbergProps.PROP_INITIAL_TITLE, initialTitle);
+ }
if(initialData != null) {
bundle.putString(GutenbergProps.PROP_INITIAL_DATA, initialData);
}
diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
index ef95c7e65862f6..0c04308125df71 100644
--- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
+++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
@@ -399,7 +399,10 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
}
func gutenbergInitialTitle() -> String? {
- return nil
+ guard isUITesting(), let initialProps = getInitialPropsFromArgs() else {
+ return nil
+ }
+ return initialProps["initialTitle"]
}
func gutenbergHostAppNamespace() -> String {
diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock
index f4eaa1c15bc1f3..cf40d621834bda 100644
--- a/packages/react-native-editor/ios/Podfile.lock
+++ b/packages/react-native-editor/ios/Podfile.lock
@@ -13,7 +13,7 @@ PODS:
- ReactCommon/turbomodule/core (= 0.71.11)
- fmt (6.2.1)
- glog (0.3.5)
- - Gutenberg (1.109.3):
+ - Gutenberg (1.110.0):
- React-Core (= 0.71.11)
- React-CoreModules (= 0.71.11)
- React-RCTImage (= 0.71.11)
@@ -429,7 +429,7 @@ PODS:
- React-RCTImage
- RNSVG (13.9.0):
- React-Core
- - RNTAztecView (1.109.3):
+ - RNTAztecView (1.110.0):
- React-Core
- WordPress-Aztec-iOS (= 1.19.9)
- SDWebImage (5.11.1):
@@ -617,7 +617,7 @@ SPEC CHECKSUMS:
FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
- Gutenberg: 74c7183474e117f4ffaae5eac944cf598a383095
+ Gutenberg: 758124df95be2159a16909fcf00e289b9299fa39
hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
@@ -662,7 +662,7 @@ SPEC CHECKSUMS:
RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d
RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789
RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
- RNTAztecView: fd32ea370f13d9edd7f43b65b6270ae499757d69
+ RNTAztecView: 75ea6f071cbdd0f0afe83de7b93c0691a2bebd21
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb
diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json
index 90f99b36a0b0a8..0a8ceed231ae4f 100644
--- a/packages/react-native-editor/package.json
+++ b/packages/react-native-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-editor",
- "version": "1.109.3",
+ "version": "1.110.0",
"description": "Mobile WordPress gutenberg editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js
index a35fabbd4e2fad..7c0989a11dc4a6 100644
--- a/packages/rich-text/src/create.js
+++ b/packages/rich-text/src/create.js
@@ -178,6 +178,19 @@ export class RichTextData {
}
}
+for ( const name of Object.getOwnPropertyNames( String.prototype ) ) {
+ if ( RichTextData.prototype.hasOwnProperty( name ) ) {
+ continue;
+ }
+
+ Object.defineProperty( RichTextData.prototype, name, {
+ value( ...args ) {
+ // Should we convert back to RichTextData?
+ return this.toHTMLString()[ name ]( ...args );
+ },
+ } );
+}
+
/**
* Create a RichText value from an `Element` tree (DOM), an HTML string or a
* plain text string, with optionally a `Range` object to set the selection. If
diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php
index 9fa350ef36c4e0..92e1d2fc345a0e 100644
--- a/phpunit/block-supports/background-test.php
+++ b/phpunit/block-supports/background-test.php
@@ -134,7 +134,7 @@ public function data_background_block_support() {
'source' => 'file',
),
),
- 'expected_wrapper' => 'Content
',
+ 'expected_wrapper' => 'Content
',
'wrapper' => 'Content
',
),
'background image style with contain, position, and repeat is applied' => array(
@@ -151,7 +151,7 @@ public function data_background_block_support() {
'backgroundRepeat' => 'no-repeat',
'backgroundSize' => 'contain',
),
- 'expected_wrapper' => 'Content
',
+ 'expected_wrapper' => 'Content
',
'wrapper' => 'Content
',
),
'background image style is appended if a style attribute already exists' => array(
@@ -166,8 +166,8 @@ public function data_background_block_support() {
'source' => 'file',
),
),
- 'expected_wrapper' => 'Content
',
- 'wrapper' => 'Content
',
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
),
'background image style is appended if a style attribute containing multiple styles already exists' => array(
'theme_name' => 'block-theme-child-with-fluid-typography',
@@ -181,8 +181,8 @@ public function data_background_block_support() {
'source' => 'file',
),
),
- 'expected_wrapper' => 'Content
',
- 'wrapper' => 'Content
',
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
),
'background image style is not applied if the block does not support background image' => array(
'theme_name' => 'block-theme-child-with-fluid-typography',
diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js
index b40e7a4b7448a8..eafb468902ef92 100644
--- a/test/e2e/specs/editor/various/block-locking.spec.js
+++ b/test/e2e/specs/editor/various/block-locking.spec.js
@@ -82,6 +82,12 @@ test.describe( 'Block Locking', () => {
await page.click( 'role=checkbox[name="Lock all"]' );
await page.click( 'role=button[name="Apply"]' );
+ await expect(
+ page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Lock' } )
+ ).toBeFocused();
+
expect( await editor.getEditedPostContent() )
.toBe( `
Some paragraph
diff --git a/test/e2e/specs/editor/various/datepicker.spec.js b/test/e2e/specs/editor/various/datepicker.spec.js
new file mode 100644
index 00000000000000..00030efa1fe274
--- /dev/null
+++ b/test/e2e/specs/editor/various/datepicker.spec.js
@@ -0,0 +1,114 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+// Set browser to a timezone that's different to `timezone`.
+test.use( {
+ timezoneId: 'America/New_York',
+} );
+
+// The `timezone` setting exposed via REST API only accepts `UTC`
+// and timezone strings by location.
+const TIMEZONES = [ 'Pacific/Honolulu', 'UTC', 'Australia/Sydney' ];
+
+TIMEZONES.forEach( ( timezone ) => {
+ test.describe( `Datepicker: ${ timezone }`, () => {
+ let orignalTimezone;
+ test.beforeAll( async ( { requestUtils } ) => {
+ orignalTimezone = ( await requestUtils.getSiteSettings() ).timezone;
+ await requestUtils.updateSiteSettings( { timezone } );
+ } );
+
+ test.beforeEach( async ( { admin, editor } ) => {
+ await admin.createNewPost();
+ await editor.openDocumentSettingsSidebar();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.updateSiteSettings( {
+ timezone: orignalTimezone,
+ } );
+ } );
+
+ test( 'should show the publishing date as "Immediately" if the date is not altered', async ( {
+ page,
+ } ) => {
+ await expect(
+ page.getByRole( 'button', { name: 'Change date' } )
+ ).toHaveText( 'Immediately' );
+ } );
+
+ test( 'should show the publishing date if the date is in the past', async ( {
+ page,
+ } ) => {
+ const datepicker = page.getByRole( 'button', {
+ name: 'Change date',
+ } );
+ await datepicker.click();
+
+ // Change the publishing date to a year in the future.
+ await page
+ .getByRole( 'group', { name: 'Date' } )
+ .getByRole( 'spinbutton', { name: 'Year' } )
+ .click();
+ await page.keyboard.press( 'ArrowDown' );
+ await page.keyboard.press( 'Escape' );
+
+ // The expected date format will be "Sep 26, 2018 11:52 pm".
+ await expect(
+ page.getByRole( 'button', { name: 'Change date' } )
+ ).toContainText( /^[A-Za-z]+\s\d{1,2},\s\d{1,4}/ );
+ } );
+
+ test( 'should show the publishing date if the date is in the future', async ( {
+ page,
+ } ) => {
+ const datepicker = page.getByRole( 'button', {
+ name: 'Change date',
+ } );
+ await datepicker.click();
+
+ // Change the publishing date to a year in the future.
+ await page
+ .getByRole( 'group', { name: 'Date' } )
+ .getByRole( 'spinbutton', { name: 'Year' } )
+ .click();
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'Escape' );
+
+ // The expected date format will be "Sep 26, 2018 11:52 pm".
+ await expect(
+ page.getByRole( 'button', { name: 'Change date' } )
+ ).toContainText( /^[A-Za-z]+\s\d{1,2},\s\d{1,4}/ );
+ } );
+
+ test( 'should show the publishing date as "Immediately" if the date is cleared', async ( {
+ page,
+ } ) => {
+ const datepicker = page.getByRole( 'button', {
+ name: 'Change date',
+ } );
+ await datepicker.click();
+
+ // Change the publishing date to a year in the future.
+ await page
+ .getByRole( 'group', { name: 'Date' } )
+ .getByRole( 'spinbutton', { name: 'Year' } )
+ .click();
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'Escape' );
+
+ // Clear the date.
+ await datepicker.click();
+ await page
+ .getByLabel( 'Change publish date' )
+ .getByRole( 'button', { name: 'Now' } )
+ .click();
+
+ await expect(
+ page.getByRole( 'button', { name: 'Change date' } )
+ ).toHaveText( 'Immediately' );
+ } );
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/invalid-block.spec.js b/test/e2e/specs/editor/various/invalid-block.spec.js
new file mode 100644
index 00000000000000..07c04a5a55457e
--- /dev/null
+++ b/test/e2e/specs/editor/various/invalid-block.spec.js
@@ -0,0 +1,119 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Invalid blocks', () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test( 'should show an invalid block message with clickable options', async ( {
+ editor,
+ page,
+ } ) => {
+ // Create an empty paragraph with the focus in the block.
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( 'hello' );
+
+ // Change to HTML mode and close the options.
+ await editor.clickBlockOptionsMenuItem( 'Edit as HTML' );
+
+ // Focus on the textarea and enter an invalid paragraph.
+ await editor.canvas
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
+ .getByRole( 'textbox' )
+ .fill( 'invalid paragraph' );
+
+ // Takes the focus away from the block so the invalid warning is triggered.
+ await editor.saveDraft();
+
+ // Click on the 'three-dots' menu toggle.
+ await editor.canvas
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
+ .getByRole( 'button', { name: 'More options' } )
+ .click();
+
+ await page
+ .getByRole( 'menu', { name: 'More options' } )
+ .getByRole( 'menuitem', { name: 'Resolve' } )
+ .click();
+
+ // Check we get the resolve modal with the appropriate contents.
+ await expect(
+ page
+ .getByRole( 'dialog', { name: 'Resolve Block' } )
+ .locator( '.block-editor-block-compare__html' )
+ ).toHaveText( [ '
invalid paragraph', '
invalid paragraph
' ] );
+ } );
+
+ test( 'should strip potentially malicious on* attributes', async ( {
+ editor,
+ page,
+ } ) => {
+ let hasAlert = false;
+ let error = '';
+ let warning = '';
+
+ page.on( 'dialog', () => {
+ hasAlert = true;
+ } );
+
+ page.on( 'console', ( msg ) => {
+ if ( msg.type() === 'error' ) {
+ error = msg.text();
+ }
+
+ if ( msg.type() === 'warning' ) {
+ warning = msg.text();
+ }
+ } );
+
+ await editor.setContent( `
+
+ aaaa 1
+
+ ` );
+
+ // Give the browser time to show the alert.
+ await expect(
+ editor.canvas
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
+ .getByRole( 'button', { name: 'Attempt Block Recovery' } )
+ ).toBeVisible();
+
+ expect( hasAlert ).toBe( false );
+ expect( error ).toContain(
+ 'Block validation: Block validation failed'
+ );
+ expect( warning ).toContain(
+ 'Block validation: Malformed HTML detected'
+ );
+ } );
+
+ test( 'should not trigger malicious script tags when using a shortcode block', async ( {
+ editor,
+ page,
+ } ) => {
+ let hasAlert = false;
+
+ page.on( 'dialog', () => {
+ hasAlert = true;
+ } );
+
+ await editor.setContent( `
+
+
+
+ ` );
+
+ // Give the browser time to show the alert.
+ await expect(
+ editor.canvas.getByRole( 'document', { name: 'Block: Shortcode' } )
+ ).toBeVisible();
+
+ expect( hasAlert ).toBe( false );
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/nux.spec.js b/test/e2e/specs/editor/various/nux.spec.js
new file mode 100644
index 00000000000000..ff55dbfa54e478
--- /dev/null
+++ b/test/e2e/specs/editor/various/nux.spec.js
@@ -0,0 +1,138 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'New User Experience (NUX)', () => {
+ test( 'should show the guide to first-time users', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost( { showWelcomeGuide: true } );
+
+ const welcomeGuide = page.getByRole( 'dialog', {
+ name: 'Welcome to the block editor',
+ } );
+ const guideHeading = welcomeGuide.getByRole( 'heading', { level: 1 } );
+ const nextButton = welcomeGuide.getByRole( 'button', { name: 'Next' } );
+ const prevButton = welcomeGuide.getByRole( 'button', {
+ name: 'Previous',
+ } );
+
+ await expect( guideHeading ).toHaveText(
+ 'Welcome to the block editor'
+ );
+
+ await nextButton.click();
+ await expect( guideHeading ).toHaveText( 'Make each block your own' );
+
+ await prevButton.click();
+ // Guide should be on page 1 of 4
+ await expect( guideHeading ).toHaveText(
+ 'Welcome to the block editor'
+ );
+
+ // Press the button for Page 2.
+ await welcomeGuide
+ .getByRole( 'button', { name: 'Page 2 of 4' } )
+ .click();
+ await expect( guideHeading ).toHaveText( 'Make each block your own' );
+
+ // Press the right arrow key for Page 3.
+ await page.keyboard.press( 'ArrowRight' );
+ await expect( guideHeading ).toHaveText(
+ 'Get to know the block library'
+ );
+
+ // Press the right arrow key for Page 4.
+ await page.keyboard.press( 'ArrowRight' );
+ await expect( guideHeading ).toHaveText(
+ 'Learn how to use the block editor'
+ );
+
+ // Click on the *visible* 'Get started' button.
+ await welcomeGuide
+ .getByRole( 'button', { name: 'Get started' } )
+ .click();
+
+ // Guide should be closed.
+ await expect( welcomeGuide ).toBeHidden();
+
+ // Reload the editor.
+ await page.reload();
+
+ // Guide should be closed.
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toBeVisible();
+ await expect( welcomeGuide ).toBeHidden();
+ } );
+
+ test( 'should not show the welcome guide again if it is dismissed', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost( { showWelcomeGuide: true } );
+
+ const welcomeGuide = page.getByRole( 'dialog', {
+ name: 'Welcome to the block editor',
+ } );
+
+ await expect( welcomeGuide ).toBeVisible();
+ await welcomeGuide.getByRole( 'button', { name: 'Close' } ).click();
+
+ // Reload the editor.
+ await page.reload();
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toBeFocused();
+
+ await expect( welcomeGuide ).toBeHidden();
+ } );
+
+ test( 'should focus post title field after welcome guide is dismissed and post is empty', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost( { showWelcomeGuide: true } );
+
+ const welcomeGuide = page.getByRole( 'dialog', {
+ name: 'Welcome to the block editor',
+ } );
+
+ await expect( welcomeGuide ).toBeVisible();
+ await welcomeGuide.getByRole( 'button', { name: 'Close' } ).click();
+
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toBeFocused();
+ } );
+
+ test( 'should show the welcome guide if it is manually opened', async ( {
+ admin,
+ page,
+ } ) => {
+ await admin.createNewPost();
+ const welcomeGuide = page.getByRole( 'dialog', {
+ name: 'Welcome to the block editor',
+ } );
+
+ await expect( welcomeGuide ).toBeHidden();
+
+ // Manually open the guide
+ await page
+ .getByRole( 'region', {
+ name: 'Editor top bar',
+ } )
+ .getByRole( 'button', { name: 'Options' } )
+ .click();
+ await page
+ .getByRole( 'menuitemcheckbox', { name: 'Welcome Guide' } )
+ .click();
+
+ await expect( welcomeGuide ).toBeVisible();
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/publishing.spec.js b/test/e2e/specs/editor/various/publishing.spec.js
new file mode 100644
index 00000000000000..8f448c58e58bd4
--- /dev/null
+++ b/test/e2e/specs/editor/various/publishing.spec.js
@@ -0,0 +1,164 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+const POST_TYPES = [ 'post', 'page' ];
+
+test.describe( 'Publishing', () => {
+ POST_TYPES.forEach( ( postType ) => {
+ test.describe( `${ postType } locking prevent saving`, () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost( { postType } );
+ } );
+
+ test( `disables the publish button when a ${ postType } is locked`, async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'textbox', { name: 'Add title' } )
+ .fill( 'E2E Test Post' );
+
+ await page.evaluate( () =>
+ window.wp.data
+ .dispatch( 'core/editor' )
+ .lockPostSaving( 'futurelock' )
+ );
+
+ // Open publish panel.
+ await page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Publish' } )
+ .click();
+
+ // Publish button should be disabled.
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor publish' } )
+ .getByRole( 'button', { name: 'Publish', exact: true } )
+ ).toBeDisabled();
+ } );
+
+ test( `disables the save shortcut when a ${ postType } is locked`, async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'textbox', { name: 'Add title' } )
+ .fill( 'E2E Test Post' );
+
+ await page.evaluate( () =>
+ window.wp.data
+ .dispatch( 'core/editor' )
+ .lockPostSaving( 'futurelock' )
+ );
+
+ await pageUtils.pressKeys( 'primary+s' );
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Save draft' } )
+ ).toBeEnabled();
+ } );
+ } );
+ } );
+
+ POST_TYPES.forEach( ( postType ) => {
+ test.describe( `a ${ postType } with pre-publish checks disabled`, () => {
+ test.beforeEach( async ( { admin, editor } ) => {
+ await admin.createNewPost( { postType } );
+ await editor.setPreferences( 'core/edit-post', {
+ isPublishSidebarEnabled: false,
+ } );
+ } );
+
+ test.afterEach( async ( { editor } ) => {
+ await editor.setPreferences( 'core/edit-post', {
+ isPublishSidebarEnabled: true,
+ } );
+ } );
+
+ test( `should publish the ${ postType } without opening the post-publish sidebar`, async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'textbox', { name: 'Add title' } )
+ .fill( 'E2E Test Post' );
+
+ // Publish the post.
+ await page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Publish' } )
+ .click();
+
+ const publishPanel = page.getByRole( 'region', {
+ name: 'Editor publish',
+ } );
+
+ // The pre-publishing panel should have been not shown.
+ await expect(
+ publishPanel.getByRole( 'button', {
+ name: 'Publish',
+ exact: true,
+ } )
+ ).toBeHidden();
+
+ // The post-publishing panel should have been not shown.
+ await expect(
+ publishPanel.getByRole( 'button', {
+ name: 'View Post',
+ } )
+ ).toBeHidden();
+
+ await expect(
+ page
+ .getByRole( 'button', { name: 'Dismiss this notice' } )
+ .filter( { hasText: 'published' } )
+ ).toBeVisible();
+ } );
+ } );
+ } );
+
+ POST_TYPES.forEach( ( postType ) => {
+ test.describe( `a ${ postType } in small viewports`, () => {
+ test.beforeEach( async ( { admin, editor, pageUtils } ) => {
+ await admin.createNewPost( { postType } );
+ await editor.setPreferences( 'core/edit-post', {
+ isPublishSidebarEnabled: false,
+ } );
+ await pageUtils.setBrowserViewport( 'small' );
+ } );
+
+ test.afterEach( async ( { editor, pageUtils } ) => {
+ await editor.setPreferences( 'core/edit-post', {
+ isPublishSidebarEnabled: true,
+ } );
+ await pageUtils.setBrowserViewport( 'large' );
+ } );
+
+ test( 'should ignore the pre-publish checks and show the publish panel', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'textbox', { name: 'Add title' } )
+ .fill( 'E2E Test Post' );
+
+ await page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Publish' } )
+ .click();
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor publish' } )
+ .getByRole( 'button', { name: 'Publish', exact: true } )
+ ).toBeVisible();
+ } );
+ } );
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/scheduling.spec.js b/test/e2e/specs/editor/various/scheduling.spec.js
new file mode 100644
index 00000000000000..1fa41a79ea7ccc
--- /dev/null
+++ b/test/e2e/specs/editor/various/scheduling.spec.js
@@ -0,0 +1,90 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+// The `timezone` setting exposed via REST API only accepts `UTC`
+// and timezone strings by location.
+const TIMEZONES = [ 'Pacific/Honolulu', 'UTC', 'Australia/Sydney' ];
+
+test.describe( 'Scheduling', () => {
+ TIMEZONES.forEach( ( timezone ) => {
+ test.describe( `Timezone ${ timezone }`, () => {
+ let orignalTimezone;
+ test.beforeAll( async ( { requestUtils } ) => {
+ orignalTimezone = ( await requestUtils.getSiteSettings() )
+ .timezone;
+
+ await requestUtils.updateSiteSettings( { timezone } );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.updateSiteSettings( {
+ timezone: orignalTimezone,
+ } );
+ } );
+
+ test( 'Should change publishing button text from "Publish" to "Schedule"', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost();
+ await editor.openDocumentSettingsSidebar();
+
+ const topBar = page.getByRole( 'region', {
+ name: 'Editor top bar',
+ } );
+
+ await expect(
+ topBar.getByRole( 'button', { name: 'Publish' } )
+ ).toBeVisible();
+
+ // Open the datepicker.
+ await page
+ .getByRole( 'button', { name: 'Change date' } )
+ .click();
+
+ // Change the publishing date to a year in the future.
+ await page
+ .getByRole( 'group', { name: 'Date' } )
+ .getByRole( 'spinbutton', { name: 'Year' } )
+ .click();
+ await page.keyboard.press( 'ArrowUp' );
+
+ // Close the datepicker.
+ await page.keyboard.press( 'Escape' );
+
+ await expect(
+ topBar.getByRole( 'button', { name: 'Schedule…' } )
+ ).toBeVisible();
+ } );
+ } );
+ } );
+
+ test( 'should keep date time UI focused when the previous and next month buttons are clicked', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost();
+ await editor.openDocumentSettingsSidebar();
+ await page.getByRole( 'button', { name: 'Change date' } ).click();
+
+ const calendar = page.getByRole( 'application', { name: 'Calendar' } );
+ const prevMonth = calendar.getByRole( 'button', {
+ name: 'View previous month',
+ } );
+ const nextMonth = calendar.getByRole( 'button', {
+ name: 'View next month',
+ } );
+
+ await prevMonth.click();
+ await expect( prevMonth ).toBeFocused();
+ await expect( calendar ).toBeVisible();
+
+ await nextMonth.click();
+ await expect( nextMonth ).toBeFocused();
+ await expect( calendar ).toBeVisible();
+ } );
+} );
diff --git a/test/integration/fixtures/blocks/core__post-featured-image.json b/test/integration/fixtures/blocks/core__post-featured-image.json
index 158007533a3f2b..dec6e14712a3a2 100644
--- a/test/integration/fixtures/blocks/core__post-featured-image.json
+++ b/test/integration/fixtures/blocks/core__post-featured-image.json
@@ -7,7 +7,8 @@
"scale": "cover",
"rel": "",
"linkTarget": "_self",
- "dimRatio": 0
+ "dimRatio": 0,
+ "useFirstImageFromPost": false
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__search.json b/test/integration/fixtures/blocks/core__search.json
index f692eac10993d8..ec961ed41b0244 100644
--- a/test/integration/fixtures/blocks/core__search.json
+++ b/test/integration/fixtures/blocks/core__search.json
@@ -8,7 +8,6 @@
"buttonPosition": "button-outside",
"buttonUseIcon": false,
"query": {},
- "buttonBehavior": "expand-searchfield",
"isSearchFieldHidden": false
},
"innerBlocks": []
diff --git a/test/integration/fixtures/blocks/core__search__custom-text.json b/test/integration/fixtures/blocks/core__search__custom-text.json
index c763cb60f65e86..3738816762ba1e 100644
--- a/test/integration/fixtures/blocks/core__search__custom-text.json
+++ b/test/integration/fixtures/blocks/core__search__custom-text.json
@@ -10,7 +10,6 @@
"buttonPosition": "button-outside",
"buttonUseIcon": false,
"query": {},
- "buttonBehavior": "expand-searchfield",
"isSearchFieldHidden": false
},
"innerBlocks": []