Commit b84b21d2cc5 for woocommerce

commit b84b21d2cc5776f6fd12217d48fcb82e5c70ea7e
Author: theAverageDev (Luca Tumedei) <luca.tumedei@automattic.com>
Date:   Tue Jun 16 23:34:37 2026 +0200

    Merge the Blocks e2e suite into the Core e2e suite (#65755)

    * Move blocks e2e bin and global-setup into the core e2e suite

    * Convert blocks global-setup to a setup project and repoint translations script

    * Promote blocks utils barrel into the core e2e suite

    * Add blocks projects to the core Playwright config and repoint utils alias

    * Merge blocks wp-env provisioning into the core e2e env

    * Cut blocks CI over to the core e2e config and fix change-trigger globs

    * Migrate blocks perf harness into the core e2e suite

    * Delete the legacy blocks e2e tree and dead scripts

    * Add changelog entry for the blocks e2e cutover

    * Satisfy lint on the migrated blocks e2e config and scripts

    * Align blocks e2e REST setup and product-collection spec with the core env port

    * Store blocks e2e auth state under the gitignored .state dir

    * Fix prettier formatting in the monorepo-utils CI-jobs fixture

    * Add WordPress Importer to the e2e tests env for blocks sample-data import

    * Align blocks e2e BASE_URL with the suite-level BASE_URL override

    * Trigger Blocks e2e jobs when the shared Playwright config changes

    The Blocks e2e CI rows run through tests/e2e-pw/playwright.config.ts,
    which defines the blocks-chromium, blocks-legacy-mini-cart, and blocks
    setup projects. Their change-trigger globs did not watch that file, and
    the core e2e project testIgnores tests/blocks, so a config edit that
    broke only the blocks projects could pass PR CI. Add the config path to
    all four Blocks e2e rows so such edits trigger the suite.

    * Pin Blocks e2e jobs to PHP 7.4 to preserve trunk coverage

    In the original Blocks' tests the PHP version used by wp-env in CI would
    be 7.4. As a consequence of the migration the Blocks' tests would now
    run using PHP 8.1 (the version specified for the other tests the Blocks'
    tests have been merged in). This restores the PHP version used for the
    Block to PHP 7.4 to keep the original intent.

    ---------

    Co-authored-by: Brandon Kraft <public@brandonkraft.com>

diff --git a/.github/project-pr-labeler.yml b/.github/project-pr-labeler.yml
index 0589ca55b83..ced0a43041a 100644
--- a/.github/project-pr-labeler.yml
+++ b/.github/project-pr-labeler.yml
@@ -58,7 +58,6 @@

 'focus: e2e tests':
   - plugins/woocommerce/tests/e2e-pw/**/*
-  - plugins/woocommerce/client/blocks/tests/e2e/**/*

 'Documentation':
   - docs/**/*
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0976392433b..0970acb1d65 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -181,8 +181,7 @@ jobs:
         env:
           RELEASE_TAG: ${{ inputs.refName != '' && inputs.refName || github.ref_name }}
           ARTIFACT_NAME: ${{ inputs.artifactName }}
-          # band-aid to get the path to wp-env.json for blocks e2e tests, until they're migrated to plugins/woocommerce
-          WP_ENV_CONFIG_PATH: ${{ github.workspace }}/${{ matrix.testEnv.start == 'env:start:blocks' && 'plugins/woocommerce/client/blocks' || matrix.projectPath  }}
+          WP_ENV_CONFIG_PATH: ${{ github.workspace }}/${{ matrix.projectPath }}
         run: node .github/workflows/scripts/override-wp-env-plugins.js

       - name: 'Start Test Environment'
diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json
index 9ff44a24108..12e08bc946c 100644
--- a/plugins/woocommerce/.wp-env.json
+++ b/plugins/woocommerce/.wp-env.json
@@ -20,7 +20,8 @@
 				"./tests/e2e-pw/test-plugins/wc-email-template-sync-test-helper",
 				"https://downloads.wordpress.org/plugin/akismet.zip",
 				"https://github.com/WP-API/Basic-Auth/archive/master.zip",
-				"https://downloads.wordpress.org/plugin/wp-mail-logging.zip"
+				"https://downloads.wordpress.org/plugin/wp-mail-logging.zip",
+				"https://downloads.wordpress.org/plugin/wordpress-importer.0.8.zip"
 			],
 			"themes": [],
 			"config": {
@@ -34,7 +35,21 @@
 				"wp-content/plugins/process-waiting-actions.php": "./tests/e2e-pw/bin/process-waiting-actions.php",
 				"wp-content/plugins/test-helper-apis.php": "./tests/e2e-pw/bin/test-helper-apis.php",
 				"wp-content/plugins/custom-place-order-button-test.php": "./tests/e2e-pw/test-plugins/blocks/custom-place-order-button-test.php",
-				"test-data/images/": "./tests/e2e-pw/test-data/images/"
+				"test-data/images/": "./tests/e2e-pw/test-data/images/",
+				"wp-content/themes/emptytheme": "./tests/e2e-pw/themes/blocks/emptytheme",
+				"wp-content/themes/theme-with-woo-templates": "./tests/e2e-pw/themes/blocks/theme-with-woo-templates",
+				"wp-content/themes/storefront-child__block-notices-filter": "./tests/e2e-pw/themes/blocks/storefront-child__block-notices-filter",
+				"wp-content/themes/storefront-child__block-notices-template": "./tests/e2e-pw/themes/blocks/storefront-child__block-notices-template",
+				"wp-content/themes/storefront-child__classic-notices-template": "./tests/e2e-pw/themes/blocks/storefront-child__classic-notices-template",
+				"wp-content/themes/storefront-child__with-block-template-part": "./tests/e2e-pw/themes/blocks/storefront-child__with-block-template-part",
+				"wp-content/themes/storefront-child__with-block-template-part-support": "./tests/e2e-pw/themes/blocks/storefront-child__with-block-template-part-support",
+				"wp-content/themes/twentytwentyfour-child__block-notices-filter": "./tests/e2e-pw/themes/blocks/twentytwentyfour-child__block-notices-filter",
+				"wp-content/themes/twentytwentyfour-child__block-notices-template": "./tests/e2e-pw/themes/blocks/twentytwentyfour-child__block-notices-template",
+				"wp-content/themes/twentytwentyfour-child__classic-notices-template": "./tests/e2e-pw/themes/blocks/twentytwentyfour-child__classic-notices-template",
+				"wp-content/themes/twentytwentyfour": "https://downloads.wordpress.org/theme/twentytwentyfour.latest-stable.zip",
+				"wp-content/plugins/woocommerce/blocks-bin": "./client/blocks/bin",
+				"wp-content/plugins/woocommerce/blocks-bin/playwright": "./tests/e2e-pw/bin/blocks",
+				"wp-content/plugins/woocommerce-blocks-test-plugins": "./tests/e2e-pw/test-plugins/blocks"
 			}
 		}
 	}
diff --git a/plugins/woocommerce/changelog/update-blocks-e2e-core-cutover b/plugins/woocommerce/changelog/update-blocks-e2e-core-cutover
new file mode 100644
index 00000000000..dcabd4419e2
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-blocks-e2e-core-cutover
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Merge the Blocks Playwright e2e suite into the Core e2e suite; no functional change to the plugin.
+
+
diff --git a/plugins/woocommerce/client/blocks/package.json b/plugins/woocommerce/client/blocks/package.json
index 7866945b8be..5503804bb6c 100644
--- a/plugins/woocommerce/client/blocks/package.json
+++ b/plugins/woocommerce/client/blocks/package.json
@@ -77,20 +77,8 @@
 		"watch:build:storybook": "storybook dev -c ./storybook -p 6006 --ci",
 		"test:js": "wp-scripts test-unit-js --config tests/js/jest.config.js",
 		"test:debug": "ndb .",
-		"test:e2e": "sh ./bin/check-env.sh && pnpm playwright test --config=tests/e2e/playwright.config.ts --project=chromium",
-		"test:e2e:block-theme": "pnpm run test:e2e block_theme",
-		"test:e2e:classic-theme": "pnpm run test:e2e classic_theme",
-		"test:e2e:block-theme-with-templates": "pnpm run test:e2e block_theme_with_templates",
-		"test:e2e:legacy-mini-cart": "sh ./bin/check-env.sh && pnpm playwright test --config=tests/e2e/playwright.config.ts --project=legacy-mini-cart",
-		"test:e2e:jest": "echo 'test:e2e:jest is no more. Use test:e2e instead.'",
-		"test:e2e:jest:dev": "echo 'test:e2e:jest is no more. Use test:e2e instead.'",
-		"test:e2e:jest:dev-watch": "echo 'test:e2e:jest is no more. Use test:e2e instead.'",
-		"test:e2e:jest:update": "echo 'test:e2e:jest is no more. Use test:e2e instead.'",
-		"env:start": "pnpm --filter='@woocommerce/block-library' wp-env start && ./tests/e2e/bin/test-env-setup.sh",
-		"env:restart": "pnpm run wp-env clean all && pnpm run wp-env start && ./tests/e2e/bin/test-env-setup.sh",
 		"env:stop": "pnpm run wp-env stop",
 		"test:help": "wp-scripts test-unit-js --help",
-		"test:performance": "sh ./bin/check-env.sh && pnpm playwright test --config=tests/e2e/playwright.performance.config.ts",
 		"test:update": "wp-scripts test-unit-js --updateSnapshot --config tests/js/jest.config.js",
 		"test:watch": "pnpm run test -- --watch",
 		"ts:check": "tsc --build",
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/.eslintrc.js b/plugins/woocommerce/client/blocks/tests/e2e/.eslintrc.js
deleted file mode 100644
index 8a8654da642..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/.eslintrc.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const rulesDirPlugin = require( 'eslint-plugin-rulesdir' );
-rulesDirPlugin.RULES_DIR = `${ __dirname }/../../../../tests/e2e-pw/rules/blocks`;
-
-const config = {
-	extends: [
-		'plugin:playwright/recommended',
-		'plugin:@typescript-eslint/base',
-	],
-	plugins: [ 'rulesdir' ],
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: './tsconfig.json',
-	},
-	rules: {
-		'@wordpress/no-global-active-element': 'off',
-		'@wordpress/no-global-get-selection': 'off',
-		'no-restricted-syntax': [
-			'error',
-			{
-				selector: 'CallExpression[callee.property.name="$"]',
-				message: '`$` is discouraged, please use `locator` instead',
-			},
-			{
-				selector: 'CallExpression[callee.property.name="$$"]',
-				message: '`$$` is discouraged, please use `locator` instead',
-			},
-			{
-				selector:
-					'CallExpression[callee.object.name="page"][callee.property.name="waitForTimeout"]',
-				message: 'Prefer page.locator instead.',
-			},
-		],
-		'playwright/no-conditional-in-test': 'off',
-		'@typescript-eslint/await-thenable': 'error',
-		'@typescript-eslint/no-floating-promises': 'error',
-		'@typescript-eslint/no-misused-promises': 'error',
-		'rulesdir/no-raw-playwright-test-import': 'error',
-		// Since we're restoring the database for each test, hooks other than
-		// `beforeEach` don't make sense.
-		// See https://github.com/woocommerce/woocommerce/pull/46432.
-		'playwright/no-hooks': [ 'error', { allow: [ 'beforeEach' ] } ],
-	},
-};
-
-module.exports = config;
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/README.md b/plugins/woocommerce/client/blocks/tests/e2e/README.md
deleted file mode 100644
index 0ade202257e..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# WooCommerce Blocks End-to-End Tests
-
-This document provides an overview of the WooCommerce Blocks end-to-end testing process. For detailed instructions and comprehensive guidelines, please refer to the [contributor guidelines document](../../docs/contributors/e2e-guidelines.md).
-
-## Quick Start
-
-### Preparing the Environment
-
-1. Build the WooCommerce Plugin:
-
-    ```sh
-    pnpm --filter='@woocommerce/plugin-woocommerce' watch:build
-    ```
-
-2. Start the environment:
-
-    ```sh
-    pnpm --filter=@woocommerce/block-library env:start
-    ```
-
-### Running the Tests
-
-1. Run all tests:
-
-    ```sh
-    pnpm --filter=@woocommerce/block-library test:e2e
-    ```
-
-2. Run a single test file:
-
-    ```sh
-    pnpm --filter=@woocommerce/block-library test:e2e path/to/the/file.spec.ts
-    ```
-
-3. Run in UI mode:
-
-    ```sh
-    pnpm --filter=@woocommerce/block-library test:e2e --ui
-    ```
-
-4. Run in debug mode:
-
-    ```sh
-    pnpm --filter=@woocommerce/block-library test:e2e --debug
-    ```
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/playwright.config.ts b/plugins/woocommerce/client/blocks/tests/e2e/playwright.config.ts
deleted file mode 100644
index 68925a8ff69..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/playwright.config.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * External dependencies
- */
-import { fileURLToPath } from 'url';
-import { BASE_URL, STORAGE_STATE_PATH } from '@woocommerce/e2e-utils';
-import { PlaywrightTestConfig, defineConfig, devices } from '@playwright/test';
-
-const { CI, DEFAULT_TIMEOUT_OVERRIDE } = process.env;
-
-const config: PlaywrightTestConfig = {
-	maxFailures: CI ? 30 : 0,
-	timeout: parseInt( DEFAULT_TIMEOUT_OVERRIDE || '', 10 ) || 100_000, // Defaults to 100s.
-	outputDir: `${ __dirname }/artifacts/test-results`,
-	globalSetup: fileURLToPath(
-		new URL( 'global-setup.ts', 'file:' + __filename ).href
-	),
-	/* Specs moved to the Core e2e suite as part of the Blocks/Core e2e merge; the core `e2e` project ignores them via testIgnore. */
-	testDir: '../../../../tests/e2e-pw/tests/blocks',
-	retries: CI ? 1 : 0,
-	workers: 1,
-	reportSlowTests: { max: 5, threshold: 30 * 1000 }, // 30 seconds threshold
-	fullyParallel: false,
-	forbidOnly: !! CI,
-	reporter: process.env.CI
-		? [
-				[ 'list' ],
-				[
-					'allure-playwright',
-					{
-						outputFolder: `${ __dirname }/artifacts/test-results/allure-results`,
-					},
-				],
-				[
-					'junit',
-					{
-						outputFile: `${ __dirname }/artifacts/test-results/results.xml`,
-						stripANSIControlSequences: true,
-						includeProjectInTestName: true,
-					},
-				],
-				[
-					'playwright-ctrf-json-reporter',
-					{
-						outputDir: `${ __dirname }/artifacts/test-results`,
-						outputFile: `ctrf-report-${ Date.now() }.json`,
-						branchName: process.env.GITHUB_REF_NAME || '',
-						commit: process.env.GITHUB_SHA || '',
-						appName: 'woocommerce-blocks',
-						repositoryName: process.env.GITHUB_REPOSITORY || '',
-					},
-				],
-		  ]
-		: 'list',
-	use: {
-		baseURL: BASE_URL,
-		screenshot: { mode: 'only-on-failure', fullPage: true },
-		trace:
-			/^https?:\/\/localhost/.test( BASE_URL ) || ! CI
-				? 'retain-on-first-failure'
-				: 'off',
-		video: 'on-first-retry',
-		viewport: { width: 1280, height: 720 },
-		storageState: STORAGE_STATE_PATH,
-		actionTimeout: 10_000,
-		navigationTimeout: 10_000,
-		contextOptions: {
-			reducedMotion: 'reduce',
-		},
-	},
-	projects: [
-		{
-			name: 'chromium',
-			use: { ...devices[ 'Desktop Chrome' ] },
-			fullyParallel: true,
-		},
-		{
-			name: 'legacy-mini-cart',
-			testMatch: [
-				'**/blocks/mini-cart/**/*.spec.ts',
-				'**/blocks/add-to-cart-with-options/**/*.spec.ts',
-				'**/blocks/product-button/**/*.spec.ts',
-				'**/blocks/product-collection/**/*.spec.ts',
-			],
-			fullyParallel: true,
-			use: { ...devices[ 'Desktop Chrome' ] },
-		},
-	],
-};
-
-export default defineConfig( config );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/playwright.performance.config.ts b/plugins/woocommerce/client/blocks/tests/e2e/playwright.performance.config.ts
deleted file mode 100644
index 9ff50cfdc51..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/playwright.performance.config.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * External dependencies
- */
-import { defineConfig, PlaywrightTestConfig } from '@playwright/test';
-
-/**
- * Internal dependencies
- */
-import baseConfig from './playwright.config';
-
-const config: PlaywrightTestConfig = {
-	...baseConfig,
-	projects: [
-		{
-			name: 'chromium',
-			// testDir is inherited from the base config, which points at the
-			// migrated specs under tests/e2e-pw/tests/blocks.
-			testMatch: '**/*.perf.ts',
-		},
-	],
-};
-
-export default defineConfig( config );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/test-data/data/data.ts b/plugins/woocommerce/client/blocks/tests/e2e/test-data/data/data.ts
deleted file mode 100644
index d63e9fb3e73..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/test-data/data/data.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// Compatibility shim: the real test data moved to tests/e2e-pw/test-data/blocks
-// during the QAO-185 e2e merge. Kept as CommonJS (explicit .ts) 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/test-data/blocks/data/data.ts' );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tsconfig.json b/plugins/woocommerce/client/blocks/tests/e2e/tsconfig.json
deleted file mode 100644
index 0e5dc606f74..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-	"$schema": "https://json.schemastore.org/tsconfig.json",
-	"extends": "../../tsconfig.base.json",
-	"compilerOptions": {
-		"noEmit": true,
-		"emitDeclarationOnly": false,
-		"allowJs": true,
-		"checkJs": false
-	},
-	"include": [ "**/*", "./.eslintrc.js" ],
-	"exclude": []
-}
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/admin/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/admin/index.ts
deleted file mode 100644
index ac4650be519..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/admin/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/admin. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/admin';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts
deleted file mode 100644
index 6264ea0d396..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/constants.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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/editor/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/editor/index.ts
deleted file mode 100644
index e74d4268a24..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/editor/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/editor. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/editor';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/frontend/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/frontend/index.ts
deleted file mode 100644
index 448fcef6730..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/frontend/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/frontend. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/frontend';
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
deleted file mode 100644
index d14a27c23f1..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/get-test-translation.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/index.ts
deleted file mode 100644
index 786424cb31a..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export { PageUtils } from '@wordpress/e2e-test-utils-playwright';
-
-export * from './wp-cli';
-export * from './constants';
-export * from './admin';
-export * from './editor';
-export * from './frontend';
-export * from './local-pickup';
-export * from './mini-cart';
-export * from './performance';
-export * from './request-utils';
-export * from './shipping';
-
-export * from './test';
-
-export * from './types';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/local-pickup/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/local-pickup/index.ts
deleted file mode 100644
index 19cc23fa11b..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/local-pickup/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/local-pickup. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/local-pickup';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/mini-cart/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/mini-cart/index.ts
deleted file mode 100644
index ccacc4e6f76..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/mini-cart/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/mini-cart. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/mini-cart';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts
deleted file mode 100644
index fb595ad4493..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/navigation/navigation.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// 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/performance/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/performance/index.ts
deleted file mode 100644
index 82cf25c5e1c..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/performance/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/performance. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/performance';
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
deleted file mode 100644
index 2521e24caac..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/request-utils/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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/shipping/index.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/shipping/index.ts
deleted file mode 100644
index ebfa484378a..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/shipping/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// QAO-185 e2e merge shim: the implementation moved to
-// tests/e2e-pw/utils/blocks/shipping. Re-exported here so the
-// @woocommerce/e2e-utils barrel and relative imports keep resolving.
-export * from '../../../../../../tests/e2e-pw/utils/blocks/shipping';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts b/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts
deleted file mode 100644
index 9a8cdc46b18..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/test.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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
deleted file mode 100644
index 5c5c526dffb..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/types.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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
deleted file mode 100644
index 8b0969efc54..00000000000
--- a/plugins/woocommerce/client/blocks/tests/e2e/utils/wp-cli.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// 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/client/blocks/tsconfig.base.json b/plugins/woocommerce/client/blocks/tsconfig.base.json
index 1d4f5413387..146c1fcab09 100644
--- a/plugins/woocommerce/client/blocks/tsconfig.base.json
+++ b/plugins/woocommerce/client/blocks/tsconfig.base.json
@@ -146,7 +146,7 @@
 				"assets/js/utils"
 			],
 			"@woocommerce/e2e-utils": [
-				"tests/e2e/utils"
+				"../../tests/e2e-pw/utils/blocks"
 			],
 			"@woocommerce/test-utils/msw": [
 				"tests/js/config/msw-setup"
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index eff3017c210..d5f959dcff0 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -32,7 +32,7 @@
 		"env:performance-init": "./tests/performance/bin/init-environment.sh",
 		"env:restart": "pnpm wp-env destroy && pnpm wp-env start --update",
 		"env:start": "pnpm wp-env start",
-		"env:start:blocks": "pnpm --filter='@woocommerce/block-library' env:start && pnpm playwright install chromium",
+		"env:start:blocks": "pnpm wp-env start && bash ./tests/e2e-pw/bin/blocks/test-env-setup.sh && pnpm playwright install chromium",
 		"env:stop": "pnpm wp-env stop",
 		"env:test": "pnpm env:dev",
 		"env:perf": "pnpm env:dev && pnpm env:performance-init",
@@ -62,7 +62,8 @@
 		"test:e2e": "pnpm test:e2e:default --project=e2e",
 		"test:e2e:default": "pnpm test:e2e:install && pnpm test:e2e:with-env default",
 		"test:e2e:install": "pnpm playwright install chromium",
-		"test:e2e:blocks": "pnpm --filter='@woocommerce/block-library' test:e2e",
+		"test:e2e:blocks": "pnpm playwright test --config=tests/e2e-pw/playwright.config.ts --project=blocks-chromium",
+		"test:e2e:blocks:performance": "pnpm playwright test --config=tests/e2e-pw/playwright.performance.config.ts --project=blocks-performance",
 		"test:e2e:email-update-propagation:pr": "pnpm test:e2e:default --project=e2e --grep @pr tests/email-editor/update-propagation",
 		"test:e2e:email-update-propagation:nightly": "pnpm test:e2e:default --project=e2e tests/email-editor/update-propagation",
 		"test:e2e:with-env": "pnpm test:e2e:install && bash ./tests/e2e-pw/run-tests-with-env.sh",
@@ -73,7 +74,7 @@
 		"test:e2e:gb-nightly": "pnpm test:e2e:with-env gutenberg-nightly",
 		"ci:legacy-minicart-flag-off": "node ./bin/set-legacy-minicart-flag.js && php bin/generate-feature-config.php",
 		"test:e2e:legacy-mini-cart": "pnpm ci:legacy-minicart-flag-off && pnpm test:e2e:default --project=legacy-mini-cart",
-		"test:e2e:blocks:legacy-mini-cart": "pnpm ci:legacy-minicart-flag-off && pnpm --filter='@woocommerce/block-library' test:e2e:legacy-mini-cart",
+		"test:e2e:blocks:legacy-mini-cart": "pnpm ci:legacy-minicart-flag-off && pnpm playwright test --config=tests/e2e-pw/playwright.config.ts --project=blocks-legacy-mini-cart",
 		"test:e2e:paypal-standard": "pnpm test:e2e:default --project=paypal-standard",
 		"test:php:legacy-mini-cart": "pnpm ci:legacy-minicart-flag-off && pnpm test:php:env",
 		"test:env:start": "pnpm env:test",
@@ -733,7 +734,9 @@
 						"src/Blocks/**/*.php",
 						"templates/**/*.php",
 						"templates/**/*.html",
-						"client/blocks/tests/e2e/**",
+						"tests/e2e-pw/bin/blocks/**",
+						"tests/e2e-pw/fixtures/blocks-setup.ts",
+						"tests/e2e-pw/playwright.config.ts",
 						"tests/e2e-pw/content-templates/blocks/**",
 						"tests/e2e-pw/test-data/blocks/**",
 						"tests/e2e-pw/test-plugins/blocks/**",
@@ -745,7 +748,10 @@
 						"@woocommerce/block-library"
 					],
 					"testEnv": {
-						"start": "env:start:blocks"
+						"start": "env:start:blocks",
+						"config": {
+							"phpVersion": "7.4"
+						}
 					},
 					"events": [
 						"pull_request",
@@ -755,7 +761,7 @@
 					],
 					"report": {
 						"resultsBlobName": "blocks-e2e-report",
-						"resultsPath": "./client/blocks/tests/e2e/artifacts/test-results",
+						"resultsPath": "tests/e2e-pw/test-results",
 						"allure": true
 					}
 				},
@@ -774,7 +780,9 @@
 						"src/Blocks/**/*.php",
 						"templates/**/*.php",
 						"templates/**/*.html",
-						"client/blocks/tests/e2e/**",
+						"tests/e2e-pw/bin/blocks/**",
+						"tests/e2e-pw/fixtures/blocks-setup.ts",
+						"tests/e2e-pw/playwright.config.ts",
 						"tests/e2e-pw/content-templates/blocks/**",
 						"tests/e2e-pw/test-data/blocks/**",
 						"tests/e2e-pw/test-plugins/blocks/**",
@@ -786,7 +794,10 @@
 						"@woocommerce/block-library"
 					],
 					"testEnv": {
-						"start": "env:start:blocks"
+						"start": "env:start:blocks",
+						"config": {
+							"phpVersion": "7.4"
+						}
 					},
 					"events": [
 						"pull_request",
@@ -794,7 +805,7 @@
 					],
 					"report": {
 						"resultsBlobName": "legacy-blocks-e2e-report",
-						"resultsPath": "./client/blocks/tests/e2e/artifacts/test-results",
+						"resultsPath": "tests/e2e-pw/test-results",
 						"allure": true
 					}
 				},
@@ -816,7 +827,9 @@
 						"--shard=10/10"
 					],
 					"changes": [
-						"client/blocks/tests/e2e/**",
+						"tests/e2e-pw/bin/blocks/**",
+						"tests/e2e-pw/fixtures/blocks-setup.ts",
+						"tests/e2e-pw/playwright.config.ts",
 						"tests/e2e-pw/content-templates/blocks/**",
 						"tests/e2e-pw/test-data/blocks/**",
 						"tests/e2e-pw/test-plugins/blocks/**",
@@ -828,6 +841,7 @@
 					"testEnv": {
 						"start": "env:start:blocks",
 						"config": {
+							"phpVersion": "7.4",
 							"wpVersion": "prerelease"
 						}
 					},
@@ -839,7 +853,7 @@
 					],
 					"report": {
 						"resultsBlobName": "blocks-e2e-report-wp-pre-release",
-						"resultsPath": "./client/blocks/tests/e2e/artifacts/test-results",
+						"resultsPath": "tests/e2e-pw/test-results",
 						"allure": true
 					}
 				},
@@ -860,7 +874,9 @@
 						"--shard=10/10"
 					],
 					"changes": [
-						"client/blocks/tests/e2e/**",
+						"tests/e2e-pw/bin/blocks/**",
+						"tests/e2e-pw/fixtures/blocks-setup.ts",
+						"tests/e2e-pw/playwright.config.ts",
 						"tests/e2e-pw/content-templates/blocks/**",
 						"tests/e2e-pw/test-data/blocks/**",
 						"tests/e2e-pw/test-plugins/blocks/**",
@@ -872,6 +888,7 @@
 					"testEnv": {
 						"start": "env:start:blocks",
 						"config": {
+							"phpVersion": "7.4",
 							"wpVersion": "latest-1"
 						}
 					},
@@ -883,7 +900,7 @@
 					],
 					"report": {
 						"resultsBlobName": "blocks-e2e-report-wp-latest-1",
-						"resultsPath": "./client/blocks/tests/e2e/artifacts/test-results",
+						"resultsPath": "tests/e2e-pw/test-results",
 						"allure": true
 					}
 				},
@@ -972,6 +989,8 @@
 		"eslint-config-wpcalypso": "5.0.0",
 		"eslint-plugin-jest": "23.20.0",
 		"eslint-plugin-playwright": "1.6.0",
+		"fs-extra": "11.1.1",
+		"glob": "^10.3.10",
 		"handlebars": "^4.7.9",
 		"jest": "29.5.x",
 		"playwright-ctrf-json-reporter": "0.0.27",
diff --git a/plugins/woocommerce/tests/e2e-pw/.eslintrc.cjs b/plugins/woocommerce/tests/e2e-pw/.eslintrc.cjs
index 6983113da19..9c0f427162e 100644
--- a/plugins/woocommerce/tests/e2e-pw/.eslintrc.cjs
+++ b/plugins/woocommerce/tests/e2e-pw/.eslintrc.cjs
@@ -44,10 +44,10 @@ module.exports = {
 			},
 		},
 		/*
-		 * Blocks e2e subtree (migrated from client/blocks/tests/e2e during the
+		 * Blocks e2e subtree (migrated into the core e2e suite during the
 		 * QAO-185 merge). These files use the blocks alias universe, so they get
 		 * the type-aware parser pointed at tsconfig.blocks.json and the blocks
-		 * lint rules that previously lived in client/blocks/tests/e2e/.eslintrc.js.
+		 * lint rules that previously applied to the blocks e2e tree.
 		 */
 		{
 			files: [ 'tests/blocks/**', 'utils/blocks/**' ],
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/generate-test-translations.js b/plugins/woocommerce/tests/e2e-pw/bin/blocks/generate-test-translations.js
similarity index 89%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/generate-test-translations.js
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/generate-test-translations.js
index eb3fe9dcb3b..4fd4bb15c02 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/bin/generate-test-translations.js
+++ b/plugins/woocommerce/tests/e2e-pw/bin/blocks/generate-test-translations.js
@@ -4,15 +4,14 @@ const { ensureDirSync, writeJsonSync } = require( 'fs-extra' );
 const crypto = require( 'crypto' );
 const path = require( 'path' );
 const glob = require( 'glob' );
-const { translations } = require( '../test-data/data/data.ts' );
-const { getTestTranslation } = require( '../utils/get-test-translation.js' );
+const { translations } = require( '../../test-data/blocks/data/data.ts' );
+const {
+	getTestTranslation,
+} = require( '../../utils/blocks/get-test-translation.js' );

-const ROOT_DIR = path.resolve( __dirname, '../../../../../' );
+const ROOT_DIR = path.resolve( __dirname, '../../../../' );
 const BUILD_DIR = path.resolve( ROOT_DIR, 'assets/client/blocks/' );
-const TESTS_DIR = path.resolve(
-	__dirname,
-	'../../../../../tests/e2e-pw/tests/blocks'
-);
+const TESTS_DIR = path.resolve( __dirname, '../../tests/blocks' );
 const LANGUAGES_DIR = path.join( ROOT_DIR, 'i18n/languages/' );

 ensureDirSync( LANGUAGES_DIR );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/cart-shortcode.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/cart-shortcode.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/cart-shortcode.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/cart-shortcode.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/cart.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/cart.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/cart.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/cart.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/checkout-shortcode.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/checkout-shortcode.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/checkout-shortcode.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/checkout-shortcode.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/checkout.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/checkout.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/checkout.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/checkout.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/mini-cart.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/mini-cart.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/mini-cart.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/mini-cart.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/pages/my-account.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/my-account.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/pages/my-account.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/pages/my-account.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/posts/product-collection.html b/plugins/woocommerce/tests/e2e-pw/bin/blocks/posts/product-collection.html
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/posts/product-collection.html
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/posts/product-collection.html
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/attributes.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/attributes.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/attributes.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/attributes.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/index.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/index.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/index.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/index.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/blog-name.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/blog-name.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/blog-name.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/blog-name.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/coupon.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/coupon.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/coupon.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/coupon.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/customer.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/customer.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/customer.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/customer.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/languages.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/languages.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/languages.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/languages.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/pages.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/pages.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/pages.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/pages.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/payment.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/payment.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/payment.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/payment.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/posts.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/posts.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/posts.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/posts.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/reviews.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/reviews.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/reviews.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/reviews.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/shipping.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/shipping.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/shipping.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/shipping.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/tax.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/tax.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/tax.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/tax.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/products.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/products.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/products.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/products.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/rewrite.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/rewrite.sh
similarity index 100%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/rewrite.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/rewrite.sh
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e-pw/bin/blocks/test-env-setup.sh
similarity index 93%
rename from plugins/woocommerce/client/blocks/tests/e2e/bin/test-env-setup.sh
rename to plugins/woocommerce/tests/e2e-pw/bin/blocks/test-env-setup.sh
index 11042e79ac0..cf88237bff3 100755
--- a/plugins/woocommerce/client/blocks/tests/e2e/bin/test-env-setup.sh
+++ b/plugins/woocommerce/tests/e2e-pw/bin/blocks/test-env-setup.sh
@@ -21,7 +21,7 @@ $prefs["core/edit-site"]["welcomeGuideTemplate"] = false;
 update_user_meta( 1, "wp_persisted_preferences", $prefs );
 '
 # Activate the Test Helper APIs utility plugin.
-wp-env run tests-cli -- wp plugin activate woocommerce-test-plugins/test-helper-apis
+wp-env run tests-cli -- wp plugin activate test-helper-apis

 echo "Generating test translations"
 node $script_dir/generate-test-translations.js
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/global-setup.ts b/plugins/woocommerce/tests/e2e-pw/fixtures/blocks-setup.ts
similarity index 89%
rename from plugins/woocommerce/client/blocks/tests/e2e/global-setup.ts
rename to plugins/woocommerce/tests/e2e-pw/fixtures/blocks-setup.ts
index b469d6f3815..c21bb5145a2 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/global-setup.ts
+++ b/plugins/woocommerce/tests/e2e-pw/fixtures/blocks-setup.ts
@@ -3,7 +3,7 @@
 /**
  * External dependencies
  */
-import { chromium, request } from '@playwright/test';
+import { test as setup, chromium, request } from '@playwright/test';
 import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
 import {
 	adminFile,
@@ -11,13 +11,14 @@ import {
 	customerFile,
 	BLOCK_THEME_SLUG,
 	DB_EXPORT_FILE,
+	// eslint-disable-next-line import/no-unresolved -- resolved via the @woocommerce/e2e-utils tsconfig alias.
 } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
  */
-import { customer, admin } from './test-data/data/data';
-import { BASE_URL } from './utils/constants';
+import { customer, admin } from '../test-data/blocks/data/data';
+import { BASE_URL } from '../utils/blocks/constants';

 const prepareAttributes = async () => {
 	const browser = await chromium.launch();
@@ -60,7 +61,7 @@ const prepareAttributes = async () => {
 	await wpCLI( cronTask );
 };

-async function globalSetup() {
+setup( 'blocks setup', async () => {
 	console.log( 'Running global setup:' );
 	console.time( '└ Total time' );

@@ -110,6 +111,4 @@ async function globalSetup() {

 	await requestContext.dispose();
 	console.timeEnd( '└ Total time' );
-}
-
-export default globalSetup;
+} );
diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.config.ts b/plugins/woocommerce/tests/e2e-pw/playwright.config.ts
index 5e49e48f6e9..97460bb9e32 100644
--- a/plugins/woocommerce/tests/e2e-pw/playwright.config.ts
+++ b/plugins/woocommerce/tests/e2e-pw/playwright.config.ts
@@ -4,6 +4,11 @@
 import { defineConfig, devices } from '@playwright/test';
 import dotenv from 'dotenv';

+/**
+ * Internal dependencies
+ */
+import { adminFile as BLOCKS_ADMIN_STATE } from './utils/blocks/constants';
+
 // __dirname is not natively available in ESM, but Playwright's config loader shims it.
 dotenv.config( { path: __dirname + '/.env' } );

@@ -15,6 +20,13 @@ if ( ! process.env.BASE_URL ) {
 	);
 }

+// The blocks setup project uses @wordpress/e2e-test-utils-playwright, which derives
+// the REST API root from WP_BASE_URL (its default is port 8889). Align it with the
+// suite's base URL so REST setup targets the same WordPress instance.
+if ( ! process.env.WP_BASE_URL ) {
+	process.env.WP_BASE_URL = process.env.BASE_URL;
+}
+
 const { BASE_URL, CI, E2E_MAX_FAILURES, REPEAT_EACH } = process.env;

 export const TESTS_ROOT_PATH = __dirname;
@@ -95,6 +107,11 @@ export const setupProjects = [
 		testMatch: `site.setup.ts`,
 		dependencies: [ 'global authentication' ],
 	},
+	{
+		name: 'blocks setup',
+		testDir: `${ TESTS_ROOT_PATH }/fixtures`,
+		testMatch: 'blocks-setup.ts',
+	},
 ];

 export default defineConfig( {
@@ -135,7 +152,7 @@ export default defineConfig( {
 				'**/api-tests/**',
 				/* Exclude PayPal tests, as they don't run well in parallel - see https://github.com/woocommerce/woocommerce/pull/63068. */
 				'**/tests/paypal/**',
-				/* Blocks specs are run by the Blocks e2e suite - see client/blocks/tests/e2e/playwright.config.ts. */
+				/* Blocks specs are run by the blocks-chromium and blocks-legacy-mini-cart projects below. */
 				'**/tests/blocks/**',
 			],
 			dependencies: [ 'site setup' ],
@@ -156,5 +173,31 @@ export default defineConfig( {
 			testMatch: [ '**/tests/paypal/**' ],
 			dependencies: [ 'site setup' ],
 		},
+		{
+			name: 'blocks-chromium',
+			testDir: `${ TESTS_ROOT_PATH }/tests/blocks`,
+			dependencies: [ 'blocks setup' ],
+			fullyParallel: true,
+			use: {
+				...devices[ 'Desktop Chrome' ],
+				storageState: BLOCKS_ADMIN_STATE,
+			},
+		},
+		{
+			name: 'blocks-legacy-mini-cart',
+			testDir: `${ TESTS_ROOT_PATH }/tests/blocks`,
+			testMatch: [
+				'**/mini-cart/**/*.spec.ts',
+				'**/add-to-cart-with-options/**/*.spec.ts',
+				'**/product-button/**/*.spec.ts',
+				'**/product-collection/**/*.spec.ts',
+			],
+			dependencies: [ 'blocks setup' ],
+			fullyParallel: true,
+			use: {
+				...devices[ 'Desktop Chrome' ],
+				storageState: BLOCKS_ADMIN_STATE,
+			},
+		},
 	],
 } );
diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.performance.config.ts b/plugins/woocommerce/tests/e2e-pw/playwright.performance.config.ts
new file mode 100644
index 00000000000..4ab265d5d44
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/playwright.performance.config.ts
@@ -0,0 +1,29 @@
+/**
+ * External dependencies
+ */
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import baseConfig, { TESTS_ROOT_PATH } from './playwright.config';
+import { adminFile } from './utils/blocks/constants';
+
+export default defineConfig( {
+	...baseConfig,
+	projects: [
+		{
+			name: 'blocks setup',
+			testDir: `${ TESTS_ROOT_PATH }/fixtures`,
+			testMatch: 'blocks-setup.ts',
+		},
+		{
+			name: 'blocks-performance',
+			testDir: `${ TESTS_ROOT_PATH }/tests/blocks`,
+			testMatch: '**/*.perf.ts',
+			dependencies: [ 'blocks setup' ],
+			fullyParallel: false,
+			use: { ...devices[ 'Desktop Chrome' ], storageState: adminFile },
+		},
+	],
+} );
diff --git a/plugins/woocommerce/tests/e2e-pw/test-data/blocks/data/data.ts b/plugins/woocommerce/tests/e2e-pw/test-data/blocks/data/data.ts
index 54ae86d77f5..a0731f8f2e9 100644
--- a/plugins/woocommerce/tests/e2e-pw/test-data/blocks/data/data.ts
+++ b/plugins/woocommerce/tests/e2e-pw/test-data/blocks/data/data.ts
@@ -59,7 +59,7 @@ const customer = {
 };

 // Reviews are ordered by when they were created.
-// source: plugins/woocommerce/client/blocks/tests/e2e/bin/scripts/parallel/reviews.sh
+// source: plugins/woocommerce/tests/e2e-pw/bin/blocks/scripts/parallel/reviews.sh
 const hoodieReviews = [
 	{
 		name: `${ customer.first_name } ${ customer.last_name }`,
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/blocks/product-collection/product-collection.block_theme.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/blocks/product-collection/product-collection.block_theme.spec.ts
index dfad3cadc25..ce6b19faf9b 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/blocks/product-collection/product-collection.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e-pw/tests/blocks/product-collection/product-collection.block_theme.spec.ts
@@ -456,7 +456,7 @@ test.describe( 'Product Collection', () => {
 			await page
 				.getByRole( 'option', {
 					name: `Cap http://localhost:${
-						process.env.WP_ENV_TESTS_PORT || '8889'
+						process.env.WP_ENV_TESTS_PORT || '8086'
 					}/product/cap/`,
 				} )
 				.click();
diff --git a/plugins/woocommerce/tests/e2e-pw/tsconfig.json b/plugins/woocommerce/tests/e2e-pw/tsconfig.json
index f71917c29c7..7b3f4861241 100644
--- a/plugins/woocommerce/tests/e2e-pw/tsconfig.json
+++ b/plugins/woocommerce/tests/e2e-pw/tsconfig.json
@@ -7,7 +7,7 @@
 		"baseUrl": ".",
 		"paths": {
 			"@woocommerce/e2e-utils": [
-				"../../client/blocks/tests/e2e/utils"
+				"./utils/blocks"
 			]
 		},
 		"esModuleInterop": true,
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts
index 1a214960832..670dd560a2f 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/constants.ts
@@ -18,11 +18,12 @@ export const CLASSIC_CHILD_THEME_WITH_CLASSIC_NOTICES_TEMPLATE_SLUG = `${ CLASSI
 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' );
+	process.env.BASE_URL ||
+	'http://localhost:' + ( process.env.WP_ENV_TESTS_PORT || '8086' );

 export const WP_ARTIFACTS_PATH =
 	process.env.WP_ARTIFACTS_PATH ||
-	path.join( process.cwd(), 'tests/e2e/artifacts' );
+	path.join( process.cwd(), 'tests/e2e-pw/.state' );

 export const STORAGE_STATE_PATH =
 	process.env.STORAGE_STATE_PATH ||
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/blocks/index.ts b/plugins/woocommerce/tests/e2e-pw/utils/blocks/index.ts
index 9293cc4c6b2..786424cb31a 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/blocks/index.ts
+++ b/plugins/woocommerce/tests/e2e-pw/utils/blocks/index.ts
@@ -1,6 +1,16 @@
-// QAO-185 bridge aggregator.
-// Re-exports the blocks e2e utils barrel via the @woocommerce/e2e-utils tsconfig alias
-// (added to tests/e2e-pw/tsconfig.json in PR #1, Step 3).
-// Rewritten in PR #7 once utils moves (PR #4/#5) have landed.
-// eslint-disable-next-line import/no-unresolved
-export * from '@woocommerce/e2e-utils';
+export { PageUtils } from '@wordpress/e2e-test-utils-playwright';
+
+export * from './wp-cli';
+export * from './constants';
+export * from './admin';
+export * from './editor';
+export * from './frontend';
+export * from './local-pickup';
+export * from './mini-cart';
+export * from './performance';
+export * from './request-utils';
+export * from './shipping';
+
+export * from './test';
+
+export * from './types';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a67da929e5c..c6c841f2f81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2646,6 +2646,12 @@ 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)
+      fs-extra:
+        specifier: 11.1.1
+        version: 11.1.1
+      glob:
+        specifier: ^10.3.10
+        version: 10.5.0
       handlebars:
         specifier: ^4.7.9
         version: 4.7.9
diff --git a/tools/monorepo-utils/src/ci-jobs/lib/__tests__/file-changes.spec.ts b/tools/monorepo-utils/src/ci-jobs/lib/__tests__/file-changes.spec.ts
index 5bbac895189..592576646e6 100644
--- a/tools/monorepo-utils/src/ci-jobs/lib/__tests__/file-changes.spec.ts
+++ b/tools/monorepo-utils/src/ci-jobs/lib/__tests__/file-changes.spec.ts
@@ -111,7 +111,7 @@ baz/project-d/baz.js`;
 		jest.mocked( execSync ).mockImplementation( ( command ) => {
 			if ( command === 'git diff --name-only origin/trunk' ) {
 				return `plugins/woocommerce/changelog/fix-123
-plugins/woocommerce/client/blocks/tests/e2e/test.spec.ts
+plugins/woocommerce/tests/e2e-pw/tests/blocks/test.spec.ts
 plugins/woocommerce/client/blocks/src/block.tsx`;
 			}

@@ -128,7 +128,7 @@ plugins/woocommerce/client/blocks/src/block.tsx`;
 							type: JobType.Test,
 							testType: 'e2e',
 							name: 'Blocks e2e tests',
-							changes: [ /^client\/blocks\/tests\/e2e\/.*/ ],
+							changes: [ /^tests\/e2e-pw\/tests\/blocks\/.*/ ],
 							command: 'test:e2e:blocks',
 							events: [ 'pull_request' ],
 							shardingArguments: [],
@@ -155,12 +155,9 @@ plugins/woocommerce/client/blocks/src/block.tsx`;
 			expect( fileChanges ).toMatchObject( {
 				'@woocommerce/plugin-woocommerce': [
 					'changelog/fix-123',
-					'client/blocks/tests/e2e/test.spec.ts',
-				],
-				'@woocommerce/block-library': [
-					'tests/e2e/test.spec.ts',
-					'src/block.tsx',
+					'tests/e2e-pw/tests/blocks/test.spec.ts',
 				],
+				'@woocommerce/block-library': [ 'src/block.tsx' ],
 			} );
 		}
 	} );
@@ -185,7 +182,7 @@ plugins/woocommerce/client/blocks/assets/style.scss`;
 							type: JobType.Test,
 							testType: 'e2e',
 							name: 'Blocks e2e tests',
-							changes: [ /^client\/blocks\/tests\/e2e\/.*/ ],
+							changes: [ /^tests\/e2e-pw\/tests\/blocks\/.*/ ],
 							command: 'test:e2e:blocks',
 							events: [ 'pull_request' ],
 							shardingArguments: [],
@@ -222,7 +219,7 @@ plugins/woocommerce/client/blocks/assets/style.scss`;
 	it( 'should handle multiple CI config patterns from different jobs', () => {
 		jest.mocked( execSync ).mockImplementation( ( command ) => {
 			if ( command === 'git diff --name-only origin/trunk' ) {
-				return `plugins/woocommerce/client/blocks/tests/e2e/test.spec.ts
+				return `plugins/woocommerce/tests/e2e-pw/tests/blocks/test.spec.ts
 plugins/woocommerce/client/blocks/tests/unit/test.spec.ts
 plugins/woocommerce/client/blocks/src/block.tsx`;
 			}
@@ -240,7 +237,7 @@ plugins/woocommerce/client/blocks/src/block.tsx`;
 							type: JobType.Test,
 							testType: 'e2e',
 							name: 'Blocks e2e tests',
-							changes: [ /^client\/blocks\/tests\/e2e\/.*/ ],
+							changes: [ /^tests\/e2e-pw\/tests\/blocks\/.*/ ],
 							command: 'test:e2e:blocks',
 							events: [ 'pull_request' ],
 							shardingArguments: [],
@@ -273,11 +270,10 @@ plugins/woocommerce/client/blocks/src/block.tsx`;
 		if ( fileChanges !== true ) {
 			expect( fileChanges ).toMatchObject( {
 				'@woocommerce/plugin-woocommerce': [
-					'client/blocks/tests/e2e/test.spec.ts',
+					'tests/e2e-pw/tests/blocks/test.spec.ts',
 					'client/blocks/tests/unit/test.spec.ts',
 				],
 				'@woocommerce/block-library': [
-					'tests/e2e/test.spec.ts',
 					'tests/unit/test.spec.ts',
 					'src/block.tsx',
 				],