Commit 9ef3ad9e7b7 for woocommerce

commit 9ef3ad9e7b7e7bcc90fea77b980fb8ea835dcc38
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Wed Jul 15 20:32:15 2026 +0300

    e2e infra: organize test helper plugins into self-contained folders (#66594)

diff --git a/plugins/woocommerce/.wp-env.e2e.json b/plugins/woocommerce/.wp-env.e2e.json
index b9a661065d5..5cd54d68277 100644
--- a/plugins/woocommerce/.wp-env.e2e.json
+++ b/plugins/woocommerce/.wp-env.e2e.json
@@ -6,6 +6,7 @@
 	"plugins": [
 		".",
 		"./tests/e2e/test-plugins/wc-email-template-sync-test-helper",
+		"./tests/e2e/test-plugins/woocommerce-e2e-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",
@@ -27,7 +28,7 @@
 		"afterClean": "./tests/e2e/bin/test-env-setup.sh"
 	},
 	"mappings": {
-		"wp-content/plugins/e2e-test-helpers": "./tests/e2e/bin",
+		"wp-content/plugins/e2e-test-bin": "./tests/e2e/bin",
 		"test-data/images/": "./tests/e2e/test-data/images/",
 		"wp-content/themes/emptytheme": "./tests/e2e/themes/blocks/emptytheme",
 		"wp-content/themes/theme-with-woo-templates": "./tests/e2e/themes/blocks/theme-with-woo-templates",
diff --git a/plugins/woocommerce/changelog/testops-174-organize-e2e-test-plugins b/plugins/woocommerce/changelog/testops-174-organize-e2e-test-plugins
new file mode 100644
index 00000000000..cccf7ef3581
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-174-organize-e2e-test-plugins
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Reorganize E2E test helper plugins into self-contained folders under tests/e2e/test-plugins/ and auto-activate the always-on helpers via the wp-env plugins array (no runtime change).
diff --git a/plugins/woocommerce/tests/e2e/README.md b/plugins/woocommerce/tests/e2e/README.md
index c1e2fce9825..f8d8de83eba 100644
--- a/plugins/woocommerce/tests/e2e/README.md
+++ b/plugins/woocommerce/tests/e2e/README.md
@@ -169,7 +169,25 @@ read: [Playwright Best Practices](https://playwright.dev/docs/best-practices).

 ## Test helper plugins

-Some E2E suites need fixture mechanisms that can't be expressed cleanly with REST or WP-CLI alone — for example, filter-driven content overrides, server-side event mirroring, or synchronous triggers for normally-scheduled jobs. These ship as small PHP plugins under `tests/e2e/test-plugins/`, mounted via `.wp-env.e2e.json`'s `plugins` array.
+Some E2E suites need fixture mechanisms that can't be expressed cleanly with REST or WP-CLI alone — for example, filter-driven content overrides, server-side event mirroring, or synchronous triggers for normally-scheduled jobs. These ship as small PHP plugins under `tests/e2e/test-plugins/`.
+
+### Convention
+
+Every always-on or externally downloaded helper is a **self-contained folder** at `tests/e2e/test-plugins/<slug>/<slug>.php` (the main file matches the folder name), with a full plugin header (`Plugin Name`, `Description`, `Version`, `Requires PHP`, `Author`). Never bind-mount an individual `.php` file — mount a folder or download a zip. The per-test block plugins under `blocks/` are single files, but their whole parent folder is mounted at once (see below).
+
+Keep `Requires PHP` at the **lowest PHP version any E2E environment runs** (currently `7.4`, the same floor as WooCommerce itself) and keep the helper's code compatible with it. WordPress silently refuses to load a plugin whose `Requires PHP` is higher than the running version: it still reports as active, but none of its hooks run and its REST routes return `rest_no_route` (404).
+
+How a helper is wired up depends on when it needs to be active:
+
+- **Always-on helpers** are listed in `.wp-env.e2e.json`'s `plugins` array, which mounts the folder **and auto-activates** it. Do not add a manual `wp plugin activate …` line for these. Current always-on helpers:
+    - `woocommerce-e2e-test-helper` — the general-purpose helper bundle, covering three concerns in one plugin:
+        - **Filter setter** — registers WordPress filters from an `e2e-filters` cookie so tests can override filtered values on the fly.
+        - **Process waiting actions** — runs the Action Scheduler queue synchronously when a request carries the `?process-waiting-actions` query param (used by the analytics suite so order data lands in reports immediately).
+        - **Test helper REST API** — endpoints (`e2e-feature-flags`, `e2e-options`, `e2e-environment`, `e2e-theme`) for toggling feature flags, setting/deleting options, reading environment info and switching themes during a test.
+    - `wc-email-template-sync-test-helper` — see below (email template sync fixtures for RSM-146).
+- **Per-test block plugins** live in `tests/e2e/test-plugins/blocks/`, mounted (not auto-activated) via the `woocommerce-blocks-test-plugins` mapping. Each is activated and deactivated by the spec that needs it (e.g. `wp plugin activate woocommerce-blocks-test-plugins/<file>.php`), because they change store behavior globally and must not be on for every test.
+
+`woocommerce-cleanup` also lives under `test-plugins/`, but it is **not** in the wp-env `plugins` array — it's an on-demand site-reset tool installed only by the external (non-wp-env) setup path, `bin/test-env-setup-external.sh`.

 ### `wc-email-template-sync-test-helper`

diff --git a/plugins/woocommerce/tests/e2e/bin/blocks/test-env-setup.sh b/plugins/woocommerce/tests/e2e/bin/blocks/test-env-setup.sh
index 6b6db7c6e71..4c0958a7719 100755
--- a/plugins/woocommerce/tests/e2e/bin/blocks/test-env-setup.sh
+++ b/plugins/woocommerce/tests/e2e/bin/blocks/test-env-setup.sh
@@ -24,10 +24,6 @@ $prefs["core/edit-site"]["welcomeGuidePage"] = false;
 $prefs["core/edit-site"]["welcomeGuideTemplate"] = false;
 update_user_meta( 1, "wp_persisted_preferences", $prefs );
 '
-# Activate the Test Helper APIs utility plugin if not already activated.
-if ! $wp_cli -- wp plugin is-active e2e-test-helpers/test-helper-apis.php >/dev/null 2>&1; then
-	$wp_cli -- wp plugin activate e2e-test-helpers/test-helper-apis.php
-fi

 echo "Generating test translations"
 node $script_dir/generate-test-translations.js
diff --git a/plugins/woocommerce/tests/e2e/bin/filter-setter.php b/plugins/woocommerce/tests/e2e/bin/filter-setter.php
deleted file mode 100644
index 92c9d7ff74c..00000000000
--- a/plugins/woocommerce/tests/e2e/bin/filter-setter.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * Plugin name: Filter Setter
- * Description: Utility intended to be used during E2E testing, to make it easy to setup WordPress filters.
- *
- * Intended to function as a (mu-)plugin while tests are running, this code works by inspecting the current cookie
- * for an entry called 'e2e-filters', which is expected to be a JSON description of filter hooks and the values we want
- * to set via those filters. For example, given the JSON (pretty printed here for clarity):
- *
- *     {
- *         "woocommerce_system_timeout": 10
- *     }
- *
- * Then a filter will be added that returns 10 when 'woocommerce_system_timeout' is invoked. Or, given:
- *
- *     {
- *         "woocommerce_enable_deathray": {
- *             "callback": "__return_false"
- *         }
- *     }
- *
- * Then the `__return_false()` convenience function will be set up in relation to filter hook
- * 'woocommerce_enable_deathray'. Additionally, priorities can be specified. Example:
- *
- *     {
- *         "woocommerce_enable_deathray": {
- *             "callback": "__return_false",
- *              "priority": 20
- *         }
- *     }
- *
- * Priorities can also be used in combination with literal values. For example:
- *
- *     {
- *         "woocommerce_default_username": {
- *             "value": "Geoffrey",
- *             "priority": 20
- *         }
- *     }
- *
- * It hopefully goes without saying, this should not be used in a production environment.
- *
- * @package Automattic\WooCommerce\E2EPlaywright
- */
-
-if ( ! isset( $_COOKIE ) || ! isset( $_COOKIE['e2e-filters'] ) ) {
-	return;
-}
-
-// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
-$filters = json_decode( $_COOKIE['e2e-filters'], true );
-
-if ( ! is_array( $filters ) ) {
-	return;
-}
-
-foreach ( $filters as $hook => $spec ) {
-	// A priority may be specified as part of the spec, else use the default priority (10).
-	$priority = isset( $spec['priority'] ) && is_int( $spec['priority'] )
-		? $spec['priority']
-		: 10;
-
-	// If the spec is not an array, then it is probably intended as the literal value.
-	if ( ! is_array( $spec ) ) {
-		$value = $spec;
-	} elseif ( isset( $spec['value'] ) ) {
-		$value = $spec['value'];
-	}
-
-	// If we know the value, we can establish our filter callback.
-	if ( isset( $value ) ) {
-		$callback = function () use ( $value ) {
-			return $value;
-		};
-	}
-
-	// We also support specifying a callback function.
-	if ( is_array( $spec ) && isset( $spec['callback'] ) && is_string( $spec['callback'] ) ) {
-		$callback = $spec['callback'];
-	}
-
-	// Ensure we have a callback, then setup the filter.
-	if ( isset( $callback ) ) {
-		add_filter( $hook, $callback, $priority );
-	}
-}
-
diff --git a/plugins/woocommerce/tests/e2e/bin/process-waiting-actions.php b/plugins/woocommerce/tests/e2e/bin/process-waiting-actions.php
deleted file mode 100644
index 1b9b02caad4..00000000000
--- a/plugins/woocommerce/tests/e2e/bin/process-waiting-actions.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-/**
- * Plugin name: Process Waiting Actions
- * Description: Utility intended to be used during E2E testing, to make it easy to process any pending scheduled actions.
- *
- * Intended to function as a (mu-)plugin while tests are running. It listens for requests made with the
- * 'process-waiting-actions' query parameter and then starts an Action Scheduler queue runner. It exits immediately
- * after this, to avoid overhead of building up a full response.
- *
- * @package Automattic\WooCommerce\E2EPlaywright
- */
-
-add_action(
-	'init',
-	function () {
-		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
-		if ( ! isset( $_GET['process-waiting-actions'] ) ) {
-			return;
-		}
-
-		if ( ! class_exists( ActionScheduler_QueueRunner::class ) ) {
-			return;
-		}
-
-		exit( ActionScheduler_QueueRunner::instance()->run( 'E2E Tests' ) ? 1 : 0 );
-	}
-);
diff --git a/plugins/woocommerce/tests/e2e/bin/test-env-setup-external.sh b/plugins/woocommerce/tests/e2e/bin/test-env-setup-external.sh
index 0333be93afc..373635bbbbe 100755
--- a/plugins/woocommerce/tests/e2e/bin/test-env-setup-external.sh
+++ b/plugins/woocommerce/tests/e2e/bin/test-env-setup-external.sh
@@ -35,9 +35,7 @@ echo "--------------------------------------------------------"

 # Define the list of PHP files to process
 mu_plugins=(
-    "filter-setter"
-    "process-waiting-actions"
-    "test-helper-apis"
+    "woocommerce-e2e-test-helper"
 	"woocommerce-cleanup"
 )

@@ -46,7 +44,7 @@ for plugin in "${mu_plugins[@]}"; do
     echo "Processing $plugin..."

     # Download the PHP file
-    curl -o "$plugin.php" "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce/tests/e2e/bin/$plugin.php"
+    curl -o "$plugin.php" "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce/tests/e2e/test-plugins/$plugin/$plugin.php"

     # Create a zip file
     (chmod 755 "$plugin.php" && zip "${plugin%}.zip" "$plugin.php")
diff --git a/plugins/woocommerce/tests/e2e/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e/bin/test-env-setup.sh
index 9255bd78df0..3ace5accbf2 100755
--- a/plugins/woocommerce/tests/e2e/bin/test-env-setup.sh
+++ b/plugins/woocommerce/tests/e2e/bin/test-env-setup.sh
@@ -13,9 +13,9 @@ if [ ! -z ${CI+y} ]; then
     # environments we use the script as it is. Inside the container the command is
     # executed from the /var/www/html path as pwd.
     echo -e '--> Dispatching script execution into cli\n'
-    # Source from the e2e-test-helpers directory mount; a single-file mount of this
+    # Source from the e2e-test-bin directory mount; a single-file mount of this
     # script can surface as an empty file under Docker gRPC FUSE.
-    $WP_ENV_CMD run --debug cli cp wp-content/plugins/e2e-test-helpers/test-env-setup.sh test-env-setup-ci.sh
+    $WP_ENV_CMD run --debug cli cp wp-content/plugins/e2e-test-bin/test-env-setup.sh test-env-setup-ci.sh
     $WP_ENV_CMD run --debug cli env -u CI WP_CLI_PREFIX= bash test-env-setup-ci.sh
     exit $?
 fi
@@ -43,17 +43,6 @@ $WP_CLI_PREFIX bash -c 'printf "apache_modules:\n  - mod_rewrite\n" > /var/www/h
 echo -e 'Update URL structure \n'
 $WP_CLI_PREFIX wp rewrite structure '/%postname%/' --hard

-echo -e 'Activate Filter Setter utility plugin \n'
-$WP_CLI_PREFIX wp plugin activate e2e-test-helpers/filter-setter.php
-
-# This plugin allows you to process queued scheduled actions immediately.
-# It's used in the analytics e2e tests so that order numbers are shown in Analytics.
-echo -e 'Activate Process Waiting Actions utility plugin \n'
-$WP_CLI_PREFIX wp plugin activate e2e-test-helpers/process-waiting-actions.php
-
-echo -e 'Activate Test Helper APIs utility plugin \n'
-$WP_CLI_PREFIX wp plugin activate e2e-test-helpers/test-helper-apis.php
-
 echo -e 'Add Customer user \n'
 if ! $WP_CLI_PREFIX wp user get customer --field=ID >/dev/null 2>&1; then
 	$WP_CLI_PREFIX wp user create customer customer@woocommercecoree2etestsuite.com \
diff --git a/plugins/woocommerce/tests/e2e/bin/woocommerce-cleanup.php b/plugins/woocommerce/tests/e2e/test-plugins/woocommerce-cleanup/woocommerce-cleanup.php
similarity index 99%
rename from plugins/woocommerce/tests/e2e/bin/woocommerce-cleanup.php
rename to plugins/woocommerce/tests/e2e/test-plugins/woocommerce-cleanup/woocommerce-cleanup.php
index 6e47cf2f913..4a927404284 100644
--- a/plugins/woocommerce/tests/e2e/bin/woocommerce-cleanup.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/woocommerce-cleanup/woocommerce-cleanup.php
@@ -15,7 +15,8 @@
 declare(strict_types=1);

 if ( ! defined( 'ABSPATH' ) ) {
-	exit; // Exit if accessed directly.
+	// Exit if accessed directly.
+	exit;
 }

 /**
diff --git a/plugins/woocommerce/tests/e2e/bin/test-helper-apis.php b/plugins/woocommerce/tests/e2e/test-plugins/woocommerce-e2e-test-helper/woocommerce-e2e-test-helper.php
similarity index 54%
rename from plugins/woocommerce/tests/e2e/bin/test-helper-apis.php
rename to plugins/woocommerce/tests/e2e/test-plugins/woocommerce-e2e-test-helper/woocommerce-e2e-test-helper.php
index 1da98c73672..875e1e04a48 100644
--- a/plugins/woocommerce/tests/e2e/bin/test-helper-apis.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/woocommerce-e2e-test-helper/woocommerce-e2e-test-helper.php
@@ -1,8 +1,130 @@
 <?php
+/**
+ * Plugin Name: WooCommerce E2E Test Helper
+ * Description: Always-on utilities for the WooCommerce E2E suite: cookie-driven filter overrides, synchronous Action Scheduler processing, and a REST API for feature flags, options, environment info and theme switching.
+ * Version: 1.0.0
+ * Requires PHP: 7.4
+ * Author: WooCommerce
+ *
+ * This bundles three previously separate helpers (filter-setter, process-waiting-actions and
+ * test-helper-apis). They share the same lifecycle — mounted and auto-activated for every E2E run
+ * via the .wp-env.e2e.json "plugins" array — so they live together here. Each concern is kept in its
+ * own section below and none of them touch the others.
+ *
+ * It hopefully goes without saying, none of this should ever run in a production environment.
+ *
+ * @package Automattic\WooCommerce\E2EPlaywright
+ */
+
+declare(strict_types=1);
+
+/*
+ * -----------------------------------------------------------------------------
+ * Filter setter
+ * -----------------------------------------------------------------------------
+ *
+ * Registers WordPress filters from an 'e2e-filters' cookie, so a spec can override filtered values on
+ * the fly. The cookie is a JSON map of hook => spec. For example (pretty printed here for clarity):
+ *
+ *     { "woocommerce_system_timeout": 10 }
+ *
+ * adds a filter returning 10 for 'woocommerce_system_timeout'. A spec may instead be an object naming
+ * a callback and/or a priority:
+ *
+ *     { "woocommerce_enable_deathray": { "callback": "__return_false", "priority": 20 } }
+ *
+ * or a literal value with a priority:
+ *
+ *     { "woocommerce_default_username": { "value": "Geoffrey", "priority": 20 } }
+ *
+ * Runs at plugin load so the filters are in place before anything reads them.
+ */
+
+/**
+ * Read the `e2e-filters` cookie and register the filters it describes.
+ */
+function woocommerce_e2e_apply_cookie_filters(): void {
+	if ( ! isset( $_COOKIE['e2e-filters'] ) ) {
+		return;
+	}
+
+	// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+	$filters = json_decode( $_COOKIE['e2e-filters'], true );
+
+	if ( ! is_array( $filters ) ) {
+		return;
+	}
+
+	foreach ( $filters as $hook => $spec ) {
+		// A priority may be specified as part of the spec, else use the default priority (10).
+		$priority = isset( $spec['priority'] ) && is_int( $spec['priority'] )
+			? $spec['priority']
+			: 10;
+
+		// If the spec is not an array, then it is probably intended as the literal value.
+		if ( ! is_array( $spec ) ) {
+			$value = $spec;
+		} elseif ( isset( $spec['value'] ) ) {
+			$value = $spec['value'];
+		}
+
+		// If we know the value, we can establish our filter callback.
+		if ( isset( $value ) ) {
+			$callback = function () use ( $value ) {
+				return $value;
+			};
+		}
+
+		// We also support specifying a callback function.
+		if ( is_array( $spec ) && isset( $spec['callback'] ) && is_string( $spec['callback'] ) ) {
+			$callback = $spec['callback'];
+		}
+
+		// Ensure we have a callback, then setup the filter.
+		if ( isset( $callback ) ) {
+			add_filter( $hook, $callback, $priority );
+		}
+	}
+}
+
+woocommerce_e2e_apply_cookie_filters();
+
+/*
+ * -----------------------------------------------------------------------------
+ * Process waiting actions
+ * -----------------------------------------------------------------------------
+ *
+ * Listens for requests carrying the 'process-waiting-actions' query parameter and starts an Action
+ * Scheduler queue runner, exiting immediately afterwards to avoid the overhead of a full response.
+ * Used by the analytics suite so scheduled order data lands in reports synchronously.
+ */
+add_action(
+	'init',
+	function () {
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! isset( $_GET['process-waiting-actions'] ) ) {
+			return;
+		}
+
+		if ( ! class_exists( ActionScheduler_QueueRunner::class ) ) {
+			return;
+		}
+
+		exit( ActionScheduler_QueueRunner::instance()->run( 'E2E Tests' ) ? 1 : 0 );
+	}
+);
+
+/*
+ * -----------------------------------------------------------------------------
+ * Test helper REST API
+ * -----------------------------------------------------------------------------
+ *
+ * REST routes for toggling feature flags, setting/deleting options, reading environment info and
+ * switching themes during a test.
+ */

 /**
- * Plugin Name: Test Helper APIs
- * Description: Utility REST API designed for E2E testing purposes. Allows turning features on or off, and setting option values
+ * Register the E2E test helper REST routes (feature flags and options).
  */
 function register_helper_api() {
 	register_rest_route(
@@ -122,7 +244,8 @@ add_filter( 'comment_flood_filter', '__return_false', 99 );

 /**
  * Update a WordPress option.
- * @param WP_REST_Request $request
+ *
+ * @param WP_REST_Request $request The REST request, carrying `option_name` and `option_value`.
  * @return WP_REST_Response
  */
 function api_update_option( WP_REST_Request $request ) {
@@ -145,11 +268,11 @@ function api_update_option( WP_REST_Request $request ) {
 /**
  * Delete a WordPress option.
  *
- * @param WP_REST_Request $request
+ * @param WP_REST_Request $request The REST request, carrying `option_name`.
  * @return WP_REST_Response
  */
 function api_delete_option( WP_REST_Request $request ) {
-	$option_name  = sanitize_text_field( $request['option_name'] );
+	$option_name = sanitize_text_field( $request['option_name'] );

 	$option_exists = get_option( $option_name, null );