Commit 3eee7538c0a for woocommerce
commit 3eee7538c0a4b11c8d45df19a1abb134fd1d1878
Author: theAverageDev (Luca Tumedei) <luca.tumedei@automattic.com>
Date: Wed Jun 10 22:48:44 2026 +0200
Migrate Blocks e2e leaf utils and content-templates into Core suite (#65628)
diff --git a/plugins/woocommerce/changelog/qao-403-blocks-e2e-leaf-utils-content-templates b/plugins/woocommerce/changelog/qao-403-blocks-e2e-leaf-utils-content-templates
new file mode 100644
index 00000000000..47fa6e467a4
--- /dev/null
+++ b/plugins/woocommerce/changelog/qao-403-blocks-e2e-leaf-utils-content-templates
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Migrate Blocks e2e leaf utils (constants, test, types, wp-cli, get-test-translation, request-utils, navigation) and content-templates into the Core e2e suite under a blocks/ namespace; test-only, no changes to shipped code.
diff --git a/plugins/woocommerce/client/blocks/docs/contributors/e2e-guidelines.md b/plugins/woocommerce/client/blocks/docs/contributors/e2e-guidelines.md
index 2dc86742d47..c735ca55764 100644
--- a/plugins/woocommerce/client/blocks/docs/contributors/e2e-guidelines.md
+++ b/plugins/woocommerce/client/blocks/docs/contributors/e2e-guidelines.md
@@ -223,7 +223,7 @@ export class Editor extends CoreEditor {
### Content Templates
-We have created `RequestUtils.createPostFromFile()` and `RequestUtils.createTemplateFromFile()` utilities that enable creating complex content testing scenarios with Handlebars templates. The template files are kept in the [content-templates](../../tests/e2e/content-templates/) folder, so you can head there for some inspiration.
+We have created `RequestUtils.createPostFromFile()` and `RequestUtils.createTemplateFromFile()` utilities that enable creating complex content testing scenarios with Handlebars templates. The template files are kept in the [content-templates](../../../../tests/e2e-pw/content-templates/blocks/) folder, so you can head there for some inspiration.
> [!IMPORTANT]
> The Handlebars template filenames must be prefixed with the entity type. For posts, an example filename would be `post_with-filters.handlebars`, and for templates `template_archive-product_with-filters.handlebars`. Notice that the latter contains the slug of the template (`archive-product`) before the name (`with-filters`), separated with an underscore - it's necessary for the template to be properly loaded and created.
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts
index 1a214960832..6264ea0d396 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts
@@ -1,39 +1,5 @@
-/**
- * External dependencies
- */
-import path from 'path';
-
-export const BLOCK_THEME_WITH_TEMPLATES_SLUG = 'theme-with-woo-templates';
-export const BLOCK_THEME_WITH_TEMPLATES_NAME = 'Theme with Woo Templates';
-export const BLOCK_THEME_SLUG = 'twentytwentyfour';
-export const BLOCK_THEME_NAME = 'Twenty Twenty-Four';
-export const BLOCK_CHILD_THEME_WITH_BLOCK_NOTICES_FILTER_SLUG = `${ BLOCK_THEME_SLUG }-child__block-notices-filter`;
-export const BLOCK_CHILD_THEME_WITH_BLOCK_NOTICES_TEMPLATE_SLUG = `${ BLOCK_THEME_SLUG }-child__block-notices-template`;
-export const BLOCK_CHILD_THEME_WITH_CLASSIC_NOTICES_TEMPLATE_SLUG = `${ BLOCK_THEME_SLUG }-child__classic-notices-template`;
-export const CLASSIC_THEME_SLUG = 'storefront';
-export const CLASSIC_THEME_NAME = 'Storefront';
-export const CLASSIC_CHILD_THEME_WITH_BLOCK_NOTICES_FILTER_SLUG = `${ CLASSIC_THEME_SLUG }-child__block-notices-filter`;
-export const CLASSIC_CHILD_THEME_WITH_BLOCK_NOTICES_TEMPLATE_SLUG = `${ CLASSIC_THEME_SLUG }-child__block-notices-template`;
-export const CLASSIC_CHILD_THEME_WITH_CLASSIC_NOTICES_TEMPLATE_SLUG = `${ CLASSIC_THEME_SLUG }-child__classic-notices-template`;
-export const CLASSIC_CHILD_THEME_WITH_BLOCK_TEMPLATE_PARTS_SLUG = `${ CLASSIC_THEME_SLUG }-child__with-block-template-part`;
-export const CLASSIC_CHILD_THEME_WITH_BLOCK_TEMPLATE_PARTS_SUPPORT_SLUG = `${ CLASSIC_THEME_SLUG }-child__with-block-template-part-support`;
-export const BASE_URL =
- 'http://localhost:' + ( process.env.WP_ENV_TESTS_PORT || '8889' );
-
-export const WP_ARTIFACTS_PATH =
- process.env.WP_ARTIFACTS_PATH ||
- path.join( process.cwd(), 'tests/e2e/artifacts' );
-
-export const STORAGE_STATE_PATH =
- process.env.STORAGE_STATE_PATH ||
- path.join( WP_ARTIFACTS_PATH, 'storage-states/admin.json' );
-
-// User roles storage states
-export const adminFile = STORAGE_STATE_PATH;
-export const customerFile = path.join(
- path.dirname( STORAGE_STATE_PATH ),
- 'customer.json'
-);
-export const guestFile = { cookies: [], origins: [] };
-
-export const DB_EXPORT_FILE = 'blocks_e2e.sql';
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path (used
+// by the blocks e2e utils barrel) working. Removed in QAO-407 (#6) with the
+// rest of this tree.
+export * from '../../../../../tests/e2e-pw/utils/blocks/constants';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/get-test-translation.js b/plugins/woocommerce/client/blocks/tests/e2e/utils/get-test-translation.js
index a1b3b8a0f4b..d14a27c23f1 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/get-test-translation.js
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/get-test-translation.js
@@ -1,9 +1,5 @@
-/* Return a test translated string.
- *
- * @param {string} string The string to be checked.
- *
- * @return {string} Test translated string.
- */
-const getTestTranslation = ( string ) => `Translated ${ string }`;
-
-module.exports = { getTestTranslation };
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. Kept as CommonJS because
+// bin/generate-test-translations.js require()s it via plain node; an ESM
+// re-export would throw. Removed in QAO-407 (#6) with the rest of this tree.
+module.exports = require( '../../../../../tests/e2e-pw/utils/blocks/get-test-translation.js' );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts
index 731c56d9213..fb595ad4493 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts
@@ -1,40 +1,4 @@
-/**
- * External dependencies
- */
-import { Page, PlaywrightTestArgs } from '@playwright/test';
-import { BlockData } from '@woocommerce/e2e-utils';
-
-/**
- * Closes any modals in the editor if they are open.
- */
-export const closeModalIfExists = async ( page: Page ) => {
- // The modal close button can have different aria-labels, depending on the version of Gutenberg/WP.
- // Newer versions (WP >=6.2) use `Close`, while older versions (WP <6.1) use `Close dialog`.
- const closeButton = page.getByLabel( /^Close$|^Close dialog$/ );
- if ( ( await closeButton.count() ) > 0 ) {
- await closeButton.click();
- }
-};
-
-/**
- * Goes to the edit page of a specified block.
- */
-export const editBlockPage = async (
- page: PlaywrightTestArgs[ 'page' ],
- { name, selectors }: BlockData
-) => {
- const {
- editor: { block: blockSelector },
- } = selectors;
- await page.goto(
- `/wp-admin/edit.php?post_type=page&s=${ encodeURIComponent( name ) }`
- );
-
- // This is the link to the edit page of the block, this is the page's title.
- await page
- .getByRole( 'link', { name: `“${ name } block” (Edit)` } )
- .click();
-
- await page.locator( blockSelector as string ).waitFor();
- await closeModalIfExists( page );
-};
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path
+// working. Removed in QAO-407 (#6) with the rest of this tree.
+export * from '../../../../../../tests/e2e-pw/utils/blocks/navigation/navigation';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/index.ts
index b3b473eeb56..2521e24caac 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/index.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/index.ts
@@ -1,35 +1,5 @@
-/**
- * External dependencies
- */
-import { RequestUtils as CoreRequestUtils } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { createPostFromFile, PostCompiler } from './posts';
-import {
- getTemplates,
- revertTemplate,
- createTemplateFromFile,
- TemplateCompiler,
-} from './templates';
-import { resetFeatureFlag, setFeatureFlag } from './feature-flag';
-
-export class RequestUtils extends CoreRequestUtils {
- /** @borrows getTemplates as this.getTemplates */
- getTemplates: typeof getTemplates = getTemplates.bind( this );
- /** @borrows revertTemplate as this.revertTemplate */
- revertTemplate: typeof revertTemplate = revertTemplate.bind( this );
- /** @borrows createPostFromFile as this.createPostFromFile */
- createPostFromFile: typeof createPostFromFile =
- createPostFromFile.bind( this );
- /** @borrows createTemplateFromFile as this.createTemplateFromFile */
- createTemplateFromFile: typeof createTemplateFromFile =
- createTemplateFromFile.bind( this );
- /** @borrows setFeatureFlag as this.setFeatureFlag */
- setFeatureFlag: typeof setFeatureFlag = setFeatureFlag.bind( this );
- /** @borrows resetFeatureFlag as this.resetFeatureFlag */
- resetFeatureFlag: typeof resetFeatureFlag = resetFeatureFlag.bind( this );
-}
-
-export { TemplateCompiler, PostCompiler };
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path (used
+// by the blocks e2e utils barrel) working. Removed in QAO-407 (#6) with the
+// rest of this tree.
+export * from '../../../../../../tests/e2e-pw/utils/blocks/request-utils';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts
index 95ad62c09a5..9a8cdc46b18 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts
@@ -1,200 +1,5 @@
-/* eslint-disable rulesdir/no-raw-playwright-test-import */
-/**
- * External dependencies
- */
-import { test as base, expect, ConsoleMessage } from '@playwright/test';
-import {
- STORAGE_STATE_PATH,
- DB_EXPORT_FILE,
- wpCLI,
- Admin,
- Editor,
- FrontendUtils,
- LocalPickupUtils,
- MiniCartUtils,
- PageUtils,
- PerformanceUtils,
- RequestUtils,
- ShippingUtils,
-} from '@woocommerce/e2e-utils';
-
-/**
- * Set of console logging types observed to protect against unexpected yet
- * handled (i.e. not catastrophic) errors or warnings. Each key corresponds
- * to the Playwright ConsoleMessage type, its value the corresponding function
- * on the console global object.
- */
-const OBSERVED_CONSOLE_MESSAGE_TYPES = [ 'warn', 'error' ] as const;
-
-/**
- * Adds a page event handler to emit uncaught exception to process if one of
- * the observed console logging types is encountered.
- *
- * @param message The console message.
- */
-function observeConsoleLogging( message: ConsoleMessage ) {
- const type = message.type();
- if (
- ! OBSERVED_CONSOLE_MESSAGE_TYPES.includes(
- type as ( typeof OBSERVED_CONSOLE_MESSAGE_TYPES )[ number ]
- )
- ) {
- return;
- }
-
- const text = message.text();
-
- // An exception is made for _blanket_ deprecation warnings: Those
- // which log regardless of whether a deprecated feature is in use.
- if ( text.includes( 'This is a global warning' ) ) {
- return;
- }
-
- // A chrome advisory warning about SameSite cookies is informational
- // about future changes, tracked separately for improvement in core.
- //
- // See: https://core.trac.wordpress.org/ticket/37000
- // See: https://www.chromestatus.com/feature/5088147346030592
- // See: https://www.chromestatus.com/feature/5633521622188032
- if ( text.includes( 'A cookie associated with a cross-site resource' ) ) {
- return;
- }
-
- // Viewing posts on the front end can result in this error, which
- // has nothing to do with Gutenberg.
- if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
- return;
- }
-
- // Not implemented yet.
- // Network errors are ignored only if we are intentionally testing
- // offline mode.
- // if (
- // text.includes( 'net::ERR_INTERNET_DISCONNECTED' ) &&
- // isOfflineMode()
- // ) {
- // return;
- // }
-
- // As of WordPress 5.3.2 in Chrome 79, navigating to the block editor
- // (Posts > Add New) will display a console warning about
- // non - unique IDs.
- // See: https://core.trac.wordpress.org/ticket/23165
- if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) {
- return;
- }
-
- // Ignore all JQMIGRATE (jQuery migrate) deprecation warnings.
- if ( text.includes( 'JQMIGRATE' ) ) {
- return;
- }
-
- const logFunction =
- type as ( typeof OBSERVED_CONSOLE_MESSAGE_TYPES )[ number ];
-
- // Disable reason: We intentionally bubble up the console message
- // which, unless the test explicitly anticipates the logging via
- // @wordpress/jest-console matchers, will cause the intended test
- // failure.
-
- // eslint-disable-next-line no-console
- console[ logFunction ]( text );
-}
-
-const test = base.extend<
- {
- admin: Admin;
- editor: Editor;
- pageUtils: PageUtils;
- frontendUtils: FrontendUtils;
- performanceUtils: PerformanceUtils;
- snapshotConfig: void;
- shippingUtils: ShippingUtils;
- localPickupUtils: LocalPickupUtils;
- miniCartUtils: MiniCartUtils;
- },
- {
- requestUtils: RequestUtils;
- wpCoreVersion: number;
- }
->( {
- admin: async ( { page, pageUtils, editor, wpCoreVersion }, use ) => {
- await use( new Admin( { page, pageUtils, editor, wpCoreVersion } ) );
- },
- editor: async ( { page, wpCoreVersion }, use ) => {
- await use( new Editor( { page, wpCoreVersion } ) );
- },
- page: async ( { page }, use ) => {
- page.on( 'console', observeConsoleLogging );
-
- await use( page );
-
- // Clear local storage after each test.
- try {
- await page.evaluate( () => {
- window.localStorage.clear();
- } );
- } catch ( error ) {
- // Ignore errors if page is already closed/navigated away
- // eslint-disable-next-line no-console
- console.log( 'Failed to clear localStorage:', error.message );
- }
-
- // Dispose the current APIRequestContext to free up resources.
- await page.request.dispose();
-
- await wpCLI( `db reset --yes` );
- // Reset the database to the initial state via snapshot import.
- await wpCLI( `db import ${ DB_EXPORT_FILE }` );
- },
- pageUtils: async ( { page }, use ) => {
- await use( new PageUtils( { page } ) );
- },
- frontendUtils: async ( { page, requestUtils }, use ) => {
- await use( new FrontendUtils( page, requestUtils ) );
- },
- performanceUtils: async ( { page }, use ) => {
- await use( new PerformanceUtils( page ) );
- },
- shippingUtils: async ( { page, admin }, use ) => {
- await use( new ShippingUtils( page, admin ) );
- },
- localPickupUtils: async ( { page, admin }, use ) => {
- await use( new LocalPickupUtils( page, admin ) );
- },
- miniCartUtils: async ( { page, frontendUtils }, use ) => {
- await use( new MiniCartUtils( page, frontendUtils ) );
- },
- requestUtils: [
- async ( {}, use, workerInfo ) => {
- const requestUtils = await RequestUtils.setup( {
- baseURL: workerInfo.project.use.baseURL as string,
- storageStatePath: STORAGE_STATE_PATH,
- } );
-
- await use( requestUtils );
- },
- { scope: 'worker', auto: true },
- ],
- wpCoreVersion: [
- async ( {}, use ) => {
- const output = await wpCLI( 'core version' );
- const version = output.stdout.trim().split( '\n' ).at( -1 ) ?? '';
-
- // We can parse this as a float because WP never updates the minor
- // version over x.9.x. E.g., after 6.9.x, it will be 7.0.x.
- const parsedVersion = Number.parseFloat( version );
-
- if ( Number.isNaN( parsedVersion ) ) {
- throw new Error(
- `Failed to parse WordPress version: ${ version }`
- );
- }
-
- await use( parsedVersion );
- },
- { scope: 'worker' },
- ],
-} );
-
-export { test, expect };
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path (used
+// by the blocks e2e utils barrel) working. Removed in QAO-407 (#6) with the
+// rest of this tree.
+export * from '../../../../../tests/e2e-pw/utils/blocks/test';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/types.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/types.ts
index f80df3679a2..5c5c526dffb 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/types.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/types.ts
@@ -1,6 +1,5 @@
-export type BlockData< T = unknown > = {
- name: string;
- slug: string;
- mainClass: string;
- selectors: Record< 'editor' | 'frontend', Record< string, unknown > >;
-} & ( T extends undefined ? Record< string, never > : T );
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path (used
+// by the blocks e2e utils barrel) working. Removed in QAO-407 (#6) with the
+// rest of this tree.
+export * from '../../../../../tests/e2e-pw/utils/blocks/types';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/wp-cli.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/wp-cli.ts
index 214841520fb..8b0969efc54 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/wp-cli.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/utils/wp-cli.ts
@@ -1,16 +1,5 @@
-/**
- * External dependencies
- */
-import { promisify } from 'util';
-import { exec } from 'child_process';
-
-const execPromisified = promisify( exec );
-
-/**
- * Runs a WP-CLI command inside the tests-cli container.
- */
-export async function wpCLI( command: string ) {
- return await execPromisified(
- 'npm run wp-env run tests-cli -- wp ' + command
- );
-}
+// Compatibility shim: the real module moved to tests/e2e-pw/utils/blocks
+// during the QAO-185 e2e merge. The re-export keeps the old import path (used
+// by the blocks e2e utils barrel) working. Removed in QAO-407 (#6) with the
+// rest of this tree.
+export * from '../../../../../tests/e2e-pw/utils/blocks/wp-cli';
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index 8109efae023..c4009469ade 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -946,6 +946,7 @@
"eslint-config-wpcalypso": "5.0.0",
"eslint-plugin-jest": "23.20.0",
"eslint-plugin-playwright": "1.6.0",
+ "handlebars": "^4.7.9",
"jest": "29.5.x",
"playwright-ctrf-json-reporter": "0.0.27",
"prettier": "npm:wp-prettier@^2.8.5",
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/post_filters-with-all-products.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/post_filters-with-all-products.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/post_filters-with-all-products.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/post_filters-with-all-products.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_active-filters.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_active-filters.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_attribute-filter.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_attribute-filter.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_filters-with-product-collection.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_filters-with-product-collection.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_filters-with-product-collection.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_filters-with-product-collection.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_price-filter.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_price-filter.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_price-filter.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_price-filter.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_rating-filter.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_rating-filter.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_rating-filter.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_rating-filter.handlebars
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_stock-status.handlebars b/plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_stock-status.handlebars
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/content-templates/template_archive-product_stock-status.handlebars
rename to plugins/woocommerce/tests/e2e-pw/content-templates/blocks/template_archive-product_stock-status.handlebars
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts
new file mode 100644
index 00000000000..1a214960832
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import path from 'path';
+
+export const BLOCK_THEME_WITH_TEMPLATES_SLUG = 'theme-with-woo-templates';
+export const BLOCK_THEME_WITH_TEMPLATES_NAME = 'Theme with Woo Templates';
+export const BLOCK_THEME_SLUG = 'twentytwentyfour';
+export const BLOCK_THEME_NAME = 'Twenty Twenty-Four';
+export const BLOCK_CHILD_THEME_WITH_BLOCK_NOTICES_FILTER_SLUG = `${ BLOCK_THEME_SLUG }-child__block-notices-filter`;
+export const BLOCK_CHILD_THEME_WITH_BLOCK_NOTICES_TEMPLATE_SLUG = `${ BLOCK_THEME_SLUG }-child__block-notices-template`;
+export const BLOCK_CHILD_THEME_WITH_CLASSIC_NOTICES_TEMPLATE_SLUG = `${ BLOCK_THEME_SLUG }-child__classic-notices-template`;
+export const CLASSIC_THEME_SLUG = 'storefront';
+export const CLASSIC_THEME_NAME = 'Storefront';
+export const CLASSIC_CHILD_THEME_WITH_BLOCK_NOTICES_FILTER_SLUG = `${ CLASSIC_THEME_SLUG }-child__block-notices-filter`;
+export const CLASSIC_CHILD_THEME_WITH_BLOCK_NOTICES_TEMPLATE_SLUG = `${ CLASSIC_THEME_SLUG }-child__block-notices-template`;
+export const CLASSIC_CHILD_THEME_WITH_CLASSIC_NOTICES_TEMPLATE_SLUG = `${ CLASSIC_THEME_SLUG }-child__classic-notices-template`;
+export const CLASSIC_CHILD_THEME_WITH_BLOCK_TEMPLATE_PARTS_SLUG = `${ CLASSIC_THEME_SLUG }-child__with-block-template-part`;
+export const CLASSIC_CHILD_THEME_WITH_BLOCK_TEMPLATE_PARTS_SUPPORT_SLUG = `${ CLASSIC_THEME_SLUG }-child__with-block-template-part-support`;
+export const BASE_URL =
+ 'http://localhost:' + ( process.env.WP_ENV_TESTS_PORT || '8889' );
+
+export const WP_ARTIFACTS_PATH =
+ process.env.WP_ARTIFACTS_PATH ||
+ path.join( process.cwd(), 'tests/e2e/artifacts' );
+
+export const STORAGE_STATE_PATH =
+ process.env.STORAGE_STATE_PATH ||
+ path.join( WP_ARTIFACTS_PATH, 'storage-states/admin.json' );
+
+// User roles storage states
+export const adminFile = STORAGE_STATE_PATH;
+export const customerFile = path.join(
+ path.dirname( STORAGE_STATE_PATH ),
+ 'customer.json'
+);
+export const guestFile = { cookies: [], origins: [] };
+
+export const DB_EXPORT_FILE = 'blocks_e2e.sql';
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/get-test-translation.js b/plugins/woocommerce/tests/e2e-pw/utils/blocks/get-test-translation.js
new file mode 100644
index 00000000000..a1b3b8a0f4b
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/get-test-translation.js
@@ -0,0 +1,9 @@
+/* Return a test translated string.
+ *
+ * @param {string} string The string to be checked.
+ *
+ * @return {string} Test translated string.
+ */
+const getTestTranslation = ( string ) => `Translated ${ string }`;
+
+module.exports = { getTestTranslation };
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/navigation/navigation.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/navigation/navigation.ts
new file mode 100644
index 00000000000..731c56d9213
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/navigation/navigation.ts
@@ -0,0 +1,40 @@
+/**
+ * External dependencies
+ */
+import { Page, PlaywrightTestArgs } from '@playwright/test';
+import { BlockData } from '@woocommerce/e2e-utils';
+
+/**
+ * Closes any modals in the editor if they are open.
+ */
+export const closeModalIfExists = async ( page: Page ) => {
+ // The modal close button can have different aria-labels, depending on the version of Gutenberg/WP.
+ // Newer versions (WP >=6.2) use `Close`, while older versions (WP <6.1) use `Close dialog`.
+ const closeButton = page.getByLabel( /^Close$|^Close dialog$/ );
+ if ( ( await closeButton.count() ) > 0 ) {
+ await closeButton.click();
+ }
+};
+
+/**
+ * Goes to the edit page of a specified block.
+ */
+export const editBlockPage = async (
+ page: PlaywrightTestArgs[ 'page' ],
+ { name, selectors }: BlockData
+) => {
+ const {
+ editor: { block: blockSelector },
+ } = selectors;
+ await page.goto(
+ `/wp-admin/edit.php?post_type=page&s=${ encodeURIComponent( name ) }`
+ );
+
+ // This is the link to the edit page of the block, this is the page's title.
+ await page
+ .getByRole( 'link', { name: `“${ name } block” (Edit)` } )
+ .click();
+
+ await page.locator( blockSelector as string ).waitFor();
+ await closeModalIfExists( page );
+};
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/feature-flag.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/feature-flag.ts
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/feature-flag.ts
rename to plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/feature-flag.ts
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/index.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/index.ts
new file mode 100644
index 00000000000..b3b473eeb56
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/index.ts
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { RequestUtils as CoreRequestUtils } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ * Internal dependencies
+ */
+import { createPostFromFile, PostCompiler } from './posts';
+import {
+ getTemplates,
+ revertTemplate,
+ createTemplateFromFile,
+ TemplateCompiler,
+} from './templates';
+import { resetFeatureFlag, setFeatureFlag } from './feature-flag';
+
+export class RequestUtils extends CoreRequestUtils {
+ /** @borrows getTemplates as this.getTemplates */
+ getTemplates: typeof getTemplates = getTemplates.bind( this );
+ /** @borrows revertTemplate as this.revertTemplate */
+ revertTemplate: typeof revertTemplate = revertTemplate.bind( this );
+ /** @borrows createPostFromFile as this.createPostFromFile */
+ createPostFromFile: typeof createPostFromFile =
+ createPostFromFile.bind( this );
+ /** @borrows createTemplateFromFile as this.createTemplateFromFile */
+ createTemplateFromFile: typeof createTemplateFromFile =
+ createTemplateFromFile.bind( this );
+ /** @borrows setFeatureFlag as this.setFeatureFlag */
+ setFeatureFlag: typeof setFeatureFlag = setFeatureFlag.bind( this );
+ /** @borrows resetFeatureFlag as this.resetFeatureFlag */
+ resetFeatureFlag: typeof resetFeatureFlag = resetFeatureFlag.bind( this );
+}
+
+export { TemplateCompiler, PostCompiler };
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/posts.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/posts.ts
similarity index 94%
rename from plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/posts.ts
rename to plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/posts.ts
index d7bcc51bfd7..3dca50f3c19 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/posts.ts
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/posts.ts
@@ -17,13 +17,13 @@ export interface PostCompiler {
/**
* Creates a post from a Handlebars template file located in the
- * tests/e2e/content-templates directory.
+ * tests/e2e-pw/content-templates/blocks directory.
*/
export async function createPostFromFile( this: RequestUtils, name: string ) {
const filePrefix = 'post';
const filePath = path.resolve(
__dirname,
- '../../content-templates',
+ '../../../content-templates/blocks',
`${ filePrefix }_${ name }.handlebars` // e.g. post_with-custom-filters.handlebars
);
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/templates.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/templates.ts
similarity index 96%
rename from plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/templates.ts
rename to plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/templates.ts
index 4a5fbd8c060..c13e6a21ef2 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/templates.ts
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/request-utils/templates.ts
@@ -62,7 +62,7 @@ export async function revertTemplate( this: RequestUtils, slug: string ) {
/**
* Creates a WP template from a Handlebars template file located in the
- * tests/e2e/content-templates directory.
+ * tests/e2e-pw/content-templates/blocks directory.
*/
export async function createTemplateFromFile(
this: RequestUtils,
@@ -76,7 +76,7 @@ export async function createTemplateFromFile(
const filePrefix = 'template';
const filePath = path.resolve(
__dirname,
- '../../content-templates',
+ '../../../content-templates/blocks',
`${ filePrefix }_${ name }.handlebars` // e.g. template_product-archive_with-custom-filters.handlebars
);
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/test.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/test.ts
new file mode 100644
index 00000000000..95ad62c09a5
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/test.ts
@@ -0,0 +1,200 @@
+/* eslint-disable rulesdir/no-raw-playwright-test-import */
+/**
+ * External dependencies
+ */
+import { test as base, expect, ConsoleMessage } from '@playwright/test';
+import {
+ STORAGE_STATE_PATH,
+ DB_EXPORT_FILE,
+ wpCLI,
+ Admin,
+ Editor,
+ FrontendUtils,
+ LocalPickupUtils,
+ MiniCartUtils,
+ PageUtils,
+ PerformanceUtils,
+ RequestUtils,
+ ShippingUtils,
+} from '@woocommerce/e2e-utils';
+
+/**
+ * Set of console logging types observed to protect against unexpected yet
+ * handled (i.e. not catastrophic) errors or warnings. Each key corresponds
+ * to the Playwright ConsoleMessage type, its value the corresponding function
+ * on the console global object.
+ */
+const OBSERVED_CONSOLE_MESSAGE_TYPES = [ 'warn', 'error' ] as const;
+
+/**
+ * Adds a page event handler to emit uncaught exception to process if one of
+ * the observed console logging types is encountered.
+ *
+ * @param message The console message.
+ */
+function observeConsoleLogging( message: ConsoleMessage ) {
+ const type = message.type();
+ if (
+ ! OBSERVED_CONSOLE_MESSAGE_TYPES.includes(
+ type as ( typeof OBSERVED_CONSOLE_MESSAGE_TYPES )[ number ]
+ )
+ ) {
+ return;
+ }
+
+ const text = message.text();
+
+ // An exception is made for _blanket_ deprecation warnings: Those
+ // which log regardless of whether a deprecated feature is in use.
+ if ( text.includes( 'This is a global warning' ) ) {
+ return;
+ }
+
+ // A chrome advisory warning about SameSite cookies is informational
+ // about future changes, tracked separately for improvement in core.
+ //
+ // See: https://core.trac.wordpress.org/ticket/37000
+ // See: https://www.chromestatus.com/feature/5088147346030592
+ // See: https://www.chromestatus.com/feature/5633521622188032
+ if ( text.includes( 'A cookie associated with a cross-site resource' ) ) {
+ return;
+ }
+
+ // Viewing posts on the front end can result in this error, which
+ // has nothing to do with Gutenberg.
+ if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
+ return;
+ }
+
+ // Not implemented yet.
+ // Network errors are ignored only if we are intentionally testing
+ // offline mode.
+ // if (
+ // text.includes( 'net::ERR_INTERNET_DISCONNECTED' ) &&
+ // isOfflineMode()
+ // ) {
+ // return;
+ // }
+
+ // As of WordPress 5.3.2 in Chrome 79, navigating to the block editor
+ // (Posts > Add New) will display a console warning about
+ // non - unique IDs.
+ // See: https://core.trac.wordpress.org/ticket/23165
+ if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) {
+ return;
+ }
+
+ // Ignore all JQMIGRATE (jQuery migrate) deprecation warnings.
+ if ( text.includes( 'JQMIGRATE' ) ) {
+ return;
+ }
+
+ const logFunction =
+ type as ( typeof OBSERVED_CONSOLE_MESSAGE_TYPES )[ number ];
+
+ // Disable reason: We intentionally bubble up the console message
+ // which, unless the test explicitly anticipates the logging via
+ // @wordpress/jest-console matchers, will cause the intended test
+ // failure.
+
+ // eslint-disable-next-line no-console
+ console[ logFunction ]( text );
+}
+
+const test = base.extend<
+ {
+ admin: Admin;
+ editor: Editor;
+ pageUtils: PageUtils;
+ frontendUtils: FrontendUtils;
+ performanceUtils: PerformanceUtils;
+ snapshotConfig: void;
+ shippingUtils: ShippingUtils;
+ localPickupUtils: LocalPickupUtils;
+ miniCartUtils: MiniCartUtils;
+ },
+ {
+ requestUtils: RequestUtils;
+ wpCoreVersion: number;
+ }
+>( {
+ admin: async ( { page, pageUtils, editor, wpCoreVersion }, use ) => {
+ await use( new Admin( { page, pageUtils, editor, wpCoreVersion } ) );
+ },
+ editor: async ( { page, wpCoreVersion }, use ) => {
+ await use( new Editor( { page, wpCoreVersion } ) );
+ },
+ page: async ( { page }, use ) => {
+ page.on( 'console', observeConsoleLogging );
+
+ await use( page );
+
+ // Clear local storage after each test.
+ try {
+ await page.evaluate( () => {
+ window.localStorage.clear();
+ } );
+ } catch ( error ) {
+ // Ignore errors if page is already closed/navigated away
+ // eslint-disable-next-line no-console
+ console.log( 'Failed to clear localStorage:', error.message );
+ }
+
+ // Dispose the current APIRequestContext to free up resources.
+ await page.request.dispose();
+
+ await wpCLI( `db reset --yes` );
+ // Reset the database to the initial state via snapshot import.
+ await wpCLI( `db import ${ DB_EXPORT_FILE }` );
+ },
+ pageUtils: async ( { page }, use ) => {
+ await use( new PageUtils( { page } ) );
+ },
+ frontendUtils: async ( { page, requestUtils }, use ) => {
+ await use( new FrontendUtils( page, requestUtils ) );
+ },
+ performanceUtils: async ( { page }, use ) => {
+ await use( new PerformanceUtils( page ) );
+ },
+ shippingUtils: async ( { page, admin }, use ) => {
+ await use( new ShippingUtils( page, admin ) );
+ },
+ localPickupUtils: async ( { page, admin }, use ) => {
+ await use( new LocalPickupUtils( page, admin ) );
+ },
+ miniCartUtils: async ( { page, frontendUtils }, use ) => {
+ await use( new MiniCartUtils( page, frontendUtils ) );
+ },
+ requestUtils: [
+ async ( {}, use, workerInfo ) => {
+ const requestUtils = await RequestUtils.setup( {
+ baseURL: workerInfo.project.use.baseURL as string,
+ storageStatePath: STORAGE_STATE_PATH,
+ } );
+
+ await use( requestUtils );
+ },
+ { scope: 'worker', auto: true },
+ ],
+ wpCoreVersion: [
+ async ( {}, use ) => {
+ const output = await wpCLI( 'core version' );
+ const version = output.stdout.trim().split( '\n' ).at( -1 ) ?? '';
+
+ // We can parse this as a float because WP never updates the minor
+ // version over x.9.x. E.g., after 6.9.x, it will be 7.0.x.
+ const parsedVersion = Number.parseFloat( version );
+
+ if ( Number.isNaN( parsedVersion ) ) {
+ throw new Error(
+ `Failed to parse WordPress version: ${ version }`
+ );
+ }
+
+ await use( parsedVersion );
+ },
+ { scope: 'worker' },
+ ],
+} );
+
+export { test, expect };
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/types.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/types.ts
new file mode 100644
index 00000000000..f80df3679a2
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/types.ts
@@ -0,0 +1,6 @@
+export type BlockData< T = unknown > = {
+ name: string;
+ slug: string;
+ mainClass: string;
+ selectors: Record< 'editor' | 'frontend', Record< string, unknown > >;
+} & ( T extends undefined ? Record< string, never > : T );
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/wp-cli.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/wp-cli.ts
new file mode 100644
index 00000000000..214841520fb
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/wp-cli.ts
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import { promisify } from 'util';
+import { exec } from 'child_process';
+
+const execPromisified = promisify( exec );
+
+/**
+ * Runs a WP-CLI command inside the tests-cli container.
+ */
+export async function wpCLI( command: string ) {
+ return await execPromisified(
+ 'npm run wp-env run tests-cli -- wp ' + command
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 71e64ef0c1c..2043c3e30d2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2649,6 +2649,9 @@ importers:
eslint-plugin-playwright:
specifier: 1.6.0
version: 1.6.0(eslint-plugin-jest@23.20.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)
+ handlebars:
+ specifier: ^4.7.9
+ version: 4.7.9
jest:
specifier: 29.5.x
version: 29.5.0(@types/node@24.12.2)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@24.12.2)(typescript@5.7.3))