Commit adbe6ddea63 for woocommerce
commit adbe6ddea63701c78345e33f45e5a1012ada1e2a
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Sat Nov 14 21:36:53 2026 +0200
[tests] Turn transactional emails off by default in tests, with a per-test opt-in (#70964)
* test: Prototype transactional emails off by default in tests
Hook-driven transactional emails only work in the first test of a
process that fires one: WC_Emails builds its listeners lazily, after
WordPress's hook snapshot, so the first restore removes them. Every
later test sends none, so a test asserting that an email is not sent
can pass without checking anything, and a test behaves differently
when run on its own.
Unhook WC_Emails::init_transactional_emails from init in the test
bootstrap, so hook-driven emails are off in every test, and add
WC_Unit_Test_Case::enable_transactional_emails() for a test that needs
them. It resets the WC_Emails singleton and adds the dispatch hooks;
the hook restore after the test removes them again.
The two tests that assert where the dispatch callback sits now call
the helper. Prototype: the remaining hook-driven "no email" tests have
not been converted yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: Make the order untrash email tests able to fail
Three untrash tests checked that restoring an order from the trash
does not send its emails. With hook-driven emails off in tests (and,
on trunk, after the first email-sending test of a run), the email
notification they watch could never fire, so they passed whatever
untrash() did: replacing the dispatch suspension with nothing left all
three green.
The two "keeps status actions" tests (posts and HPOS data stores) now
turn emails on before the restore. The "restores email dispatch" test
also compared has_action() before and after, which cannot see whether
the listener was put back, because untrash swaps it for a wrapper in
the same slot. It now checks the stored callback.
With the suspension or the restore removed, each test now fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: Build the mailer inside tests that depend on its emails
The emails WC_Emails loads depend on the features enabled when it is
built, and the shared instance is built in whichever earlier test
needs it first. Two email editor tests loop over the core emails it
holds, so whether they check the email the editor adds (partially
refunded order) depended on test order: with emails off by default,
the mailer is built elsewhere and they checked one email fewer.
Add WC_Unit_Test_Case::rebuild_mailer(), which makes the next use of
the mailer build a fresh one inside the test and puts the previous
one back after it, and use it in enable_transactional_emails() so a
mailer rebuilt there no longer leaks into later tests. The two email
editor test classes rebuild the mailer after turning the editor on.
WCTransactionalEmailPostsGeneratorTest also turned the editor on with
add_option(), which does nothing once the option exists, and set it
back to 'no' after parent::tearDown(), where the rollback no longer
undoes it, so the flag stayed off from the class's second test on.
Use update_option() and drop the late write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: Add changelog entry for transactional emails off in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/test-emails-off-by-default b/plugins/woocommerce/changelog/test-emails-off-by-default
new file mode 100644
index 00000000000..f64ae7fc0b7
--- /dev/null
+++ b/plugins/woocommerce/changelog/test-emails-off-by-default
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Turn hook-driven transactional emails off by default in PHPUnit tests, with a per-test opt-in; no production change.
diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php
index 32ef1aa9548..4bb0b2b5850 100644
--- a/plugins/woocommerce/tests/legacy/bootstrap.php
+++ b/plugins/woocommerce/tests/legacy/bootstrap.php
@@ -266,6 +266,11 @@ class WC_Unit_Tests_Bootstrap {
update_option( 'woocommerce_onboarding_opt_in', 'yes' );
require_once $this->plugin_dir . '/woocommerce.php';
+
+ // Hook-driven transactional emails are off in tests. Their listeners are built lazily, in whichever test fires
+ // the first email, so only that test would send any. A test opts in with enable_transactional_emails().
+ remove_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) );
+
FeaturePlugin::instance()->init();
// The test case wraps each test in a transaction that it rolls back. Own
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
index 638ab3336b2..6351200c1d1 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
@@ -84,6 +84,13 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
*/
private $block_types_at_setup = null;
+ /**
+ * The mailer to put back after the test, or false when the test did not rebuild it.
+ *
+ * @var WC_Emails|null|false
+ */
+ private $mailer_before_rebuild = false;
+
/**
* Enable synchronous product attribute lookup updates for test fixtures.
*
@@ -233,6 +240,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
$this->restore_server_superglobal();
$this->restore_constant_overrides();
$this->restore_block_type_registry();
+ $this->restore_mailer();
// Function and static-method mocks are process-wide. Reset them here so the test
// that installs one is the last to see it, including when the next test extends a
@@ -248,6 +256,52 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
}
}
+ /**
+ * Turn on hook-driven transactional emails for the current test.
+ *
+ * The test bootstrap leaves them off. This adds the email dispatch hooks and makes the next use of the mailer
+ * build a fresh one, so its listeners exist. The hook restore after the test removes the hooks, and the previous
+ * mailer is put back.
+ *
+ * @return void
+ */
+ protected function enable_transactional_emails(): void {
+ $this->rebuild_mailer();
+ WC_Emails::init_transactional_emails();
+ }
+
+ /**
+ * Make the next use of the mailer build a fresh one inside the current test.
+ *
+ * The emails the mailer loads depend on the features enabled when it is built, and the shared one is otherwise
+ * built in whichever earlier test needed it first. The previous mailer is put back after the test.
+ *
+ * @return void
+ */
+ protected function rebuild_mailer(): void {
+ $instance = new ReflectionProperty( WC_Emails::class, 'instance' );
+ $instance->setAccessible( true );
+ if ( false === $this->mailer_before_rebuild ) {
+ $this->mailer_before_rebuild = $instance->getValue();
+ }
+ $instance->setValue( null, null );
+ }
+
+ /**
+ * Put back the mailer that was in place before rebuild_mailer(), if the test rebuilt it.
+ *
+ * @return void
+ */
+ private function restore_mailer(): void {
+ if ( false === $this->mailer_before_rebuild ) {
+ return;
+ }
+ $instance = new ReflectionProperty( WC_Emails::class, 'instance' );
+ $instance->setAccessible( true );
+ $instance->setValue( null, $this->mailer_before_rebuild );
+ $this->mailer_before_rebuild = false;
+ }
+
/**
* Clear the WC() singleton state that survives the parent teardown.
*
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
index 64046e91f86..d6838d95731 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
@@ -336,6 +336,9 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$order->delete();
$this->assertEquals( OrderStatus::TRASH, $order->get_status(), 'The order was successfully trashed.' );
+ // Without transactional emails the notification count below stays 0 whatever untrash() does.
+ $this->enable_transactional_emails();
+
$status_action_count = 0;
$status_notification_count = 0;
$status_action = function ( $id ) use ( $order_id, &$status_action_count ) {
@@ -371,6 +374,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
* @testdox Restoring an order only suppresses transactional email dispatch for the restored order.
*/
public function test_untrash_does_not_suppress_email_dispatch_for_other_orders(): void {
+ $this->enable_transactional_emails();
$restored_order = WC_Helper_Order::create_order();
$restored_order->set_status( OrderStatus::COMPLETED );
$restored_order->save();
@@ -435,6 +439,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
public function test_untrash_preserves_email_hook_callback_order(): void {
global $wp_filter;
+ $this->enable_transactional_emails();
$order = WC_Helper_Order::create_order();
$order->set_status( OrderStatus::COMPLETED );
$order->save();
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 392b386a2bb..86fb1ff5c2a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -782,6 +782,9 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->sut->trash_order( $order );
$this->sut->read( $order );
+ // Without transactional emails the notification count below stays 0 whatever untrash_order() does.
+ $this->enable_transactional_emails();
+
$status_action_count = 0;
$status_notification_count = 0;
$status_action = function ( $order_id ) use ( $order, &$status_action_count ) {
@@ -825,19 +828,21 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->sut->trash_order( $order );
$this->sut->read( $order );
- $dispatch_callbacks_before = array(
+ $this->enable_transactional_emails();
+ $this->assertNotFalse(
has_action( 'woocommerce_order_status_completed', array( 'WC_Emails', 'send_transactional_email' ) ),
- has_action( 'woocommerce_order_status_completed', array( 'WC_Emails', 'queue_transactional_email' ) ),
+ 'The email dispatch listener is registered before the restore.'
);
$this->assertTrue( $this->sut->untrash_order( $order ) );
- $dispatch_callbacks_after = array(
- has_action( 'woocommerce_order_status_completed', array( 'WC_Emails', 'send_transactional_email' ) ),
- has_action( 'woocommerce_order_status_completed', array( 'WC_Emails', 'queue_transactional_email' ) ),
+ // untrash_order() swaps the listener for a wrapper in the same slot while it saves, so has_action() cannot
+ // tell whether it was put back. Check the stored callback instead.
+ global $wp_filter;
+ $this->assertSame(
+ array( 'WC_Emails', 'send_transactional_email' ),
+ $wp_filter['woocommerce_order_status_completed']->callbacks[10]['WC_Emails::send_transactional_email']['function']
);
-
- $this->assertSame( $dispatch_callbacks_before, $dispatch_callbacks_after );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSyncRegistryTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSyncRegistryTest.php
index 36d2ee2e10a..d289f801925 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSyncRegistryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSyncRegistryTest.php
@@ -41,6 +41,8 @@ class WCEmailTemplateSyncRegistryTest extends \WC_Unit_Test_Case {
parent::setUp();
update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
+ // Build the mailer with the editor on, so it loads the emails the editor adds.
+ $this->rebuild_mailer();
$this->fixtures_base = __DIR__ . '/fixtures/';
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailPostsGeneratorTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailPostsGeneratorTest.php
index 286d28a356a..d3a9f627039 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailPostsGeneratorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailPostsGeneratorTest.php
@@ -44,7 +44,9 @@ class WCTransactionalEmailPostsGeneratorTest extends \WC_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
- add_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
+ update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
+ // Build the mailer with the editor on, so it loads the emails the editor adds.
+ $this->rebuild_mailer();
$this->email_generator = new WCTransactionalEmailPostsGenerator();
$this->template_manager = WCTransactionalEmailPostsManager::get_instance();
$this->fixtures_base = __DIR__ . '/fixtures/';
@@ -438,6 +440,5 @@ class WCTransactionalEmailPostsGeneratorTest extends \WC_Unit_Test_Case {
WCEmailTemplateSyncRegistry::reset_cache();
parent::tearDown();
- update_option( 'woocommerce_feature_block_email_editor_enabled', 'no' );
}
}