Commit 4c3267cd336 for woocommerce
commit 4c3267cd3360e648008d7b16bd3530a9d7619126
Author: Ann <annchichi@users.noreply.github.com>
Date: Wed Apr 22 19:16:11 2026 +0800
Map email test-send errors to merchant-friendly copy (#64286)
* Map email test-send errors to merchant-friendly copy
* Add changelog entry for email test-send error messaging
* Shorten email test-send error messages
* Prefer stable error codes in friendlyEmailSendError
Switch the nonce, invalid-JSON, and preview-render branches from English
message matches to the stable backend error codes, so the mapping still
works on localized sites. Flag the two remaining message-match branches
(WSOD, apiFetch timeout) inline since they have no stable codes.
Export friendlyEmailSendError and WPError so the mapping can be unit
tested, and add tests covering each branch plus a localized-message
regression.
* Update e2e test for friendlier email test-send error copy
The 'Send email preview' test was asserting on the old raw error copy
"Error sending test email. Please try again." The PR now maps that
error (code: woocommerce_rest_email_preview_not_sent) through the
generic fallback in friendlyEmailSendError(), so the test needs to
expect the new merchant-friendly string.
* Defensively read code/message in friendlyEmailSendError
The `e as WPError` cast in handleSendEmail is TypeScript-only — apiFetch
can reject with non-WPError shapes (native TypeError on offline/CORS,
middleware-wrapped errors). When `message` is undefined,
`message.includes('critical error')` used to throw inside the catch
block; that TypeError escaped, skipping setIsSending(false) and leaving
the modal stuck on "Sending…". Optional-chain the destructure so
unshaped errors fall through to the generic fallback copy.
diff --git a/plugins/woocommerce/changelog/tweak-email-test-send-friendly-errors b/plugins/woocommerce/changelog/tweak-email-test-send-friendly-errors
new file mode 100644
index 00000000000..fee986be8eb
--- /dev/null
+++ b/plugins/woocommerce/changelog/tweak-email-test-send-friendly-errors
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Show clearer errors when a test email fails to send.
diff --git a/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-send.test.ts b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-send.test.ts
new file mode 100644
index 00000000000..b27787c8f26
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-send.test.ts
@@ -0,0 +1,113 @@
+/**
+ * Internal dependencies
+ */
+import {
+ friendlyEmailSendError,
+ WPError,
+} from '../settings-email-preview-send';
+
+const makeError = ( overrides: Partial< WPError > = {} ): WPError => ( {
+ code: '',
+ message: '',
+ data: { status: 0 },
+ ...overrides,
+} );
+
+describe( 'friendlyEmailSendError', () => {
+ it( 'maps the WP core nonce error code to the session-expired message', () => {
+ const result = friendlyEmailSendError(
+ makeError( { code: 'rest_cookie_invalid_nonce' } )
+ );
+
+ expect( result ).toBe(
+ 'Your session expired. Refresh the page and try again.'
+ );
+ } );
+
+ it( 'maps the Woo invalid_nonce code to the session-expired message', () => {
+ const result = friendlyEmailSendError(
+ makeError( { code: 'invalid_nonce' } )
+ );
+
+ expect( result ).toBe(
+ 'Your session expired. Refresh the page and try again.'
+ );
+ } );
+
+ it( 'maps rest_invalid_json to the unexpected-output message', () => {
+ const result = friendlyEmailSendError(
+ makeError( { code: 'rest_invalid_json' } )
+ );
+
+ expect( result ).toBe(
+ 'The server returned unexpected output. Check your error log, or disable recently added plugins.'
+ );
+ } );
+
+ it( 'maps a WSOD "critical error" message to the PHP-error message', () => {
+ const result = friendlyEmailSendError(
+ makeError( {
+ code: 'some_other_code',
+ message: 'There has been a critical error on this website.',
+ } )
+ );
+
+ expect( result ).toBe(
+ 'A PHP error stopped the send. Check your error log or contact your host.'
+ );
+ } );
+
+ it( 'maps woocommerce_rest_email_preview_not_rendered to the render-failed message', () => {
+ const result = friendlyEmailSendError(
+ makeError( {
+ code: 'woocommerce_rest_email_preview_not_rendered',
+ } )
+ );
+
+ expect( result ).toBe(
+ "The email couldn't be rendered. Try resetting the template in Settings → Emails."
+ );
+ } );
+
+ it( 'maps the no-valid-response message to the timeout message', () => {
+ const result = friendlyEmailSendError(
+ makeError( {
+ message: 'Could not get a valid response from the server.',
+ } )
+ );
+
+ expect( result ).toBe(
+ 'Your server timed out. If it keeps happening, ask your host to check PHP execution limits.'
+ );
+ } );
+
+ it( 'falls back to the generic message for unknown errors', () => {
+ const result = friendlyEmailSendError(
+ makeError( {
+ code: 'something_unexpected',
+ message: 'Something random went wrong.',
+ } )
+ );
+
+ expect( result ).toBe(
+ "Couldn't send the test email. Check your email settings and try again."
+ );
+ } );
+
+ it( 'still resolves by code when the message is localized (regression: locale fragility)', () => {
+ // Simulates a translated site where the backend error message has
+ // been run through __(). The mapping must still work because we
+ // match on the stable code, not the English message.
+ const result = friendlyEmailSendError(
+ makeError( {
+ code: 'woocommerce_rest_email_preview_not_rendered',
+ message:
+ "Une erreur s'est produite lors du rendu de l'aperçu de l'e-mail.",
+ } )
+ );
+
+ expect( result ).toBe(
+ "The email couldn't be rendered. Try resetting the template in Settings → Emails."
+ );
+ } );
+} );
diff --git a/plugins/woocommerce/client/admin/client/settings-email/settings-email-preview-send.tsx b/plugins/woocommerce/client/admin/client/settings-email/settings-email-preview-send.tsx
index 5f149946f85..cb4b1152c2b 100644
--- a/plugins/woocommerce/client/admin/client/settings-email/settings-email-preview-send.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-email/settings-email-preview-send.tsx
@@ -22,7 +22,7 @@ type EmailPreviewSendResponse = {
message: string;
};
-type WPError = {
+export type WPError = {
message: string;
code: string;
data: {
@@ -30,37 +30,108 @@ type WPError = {
};
};
+/**
+ * Maps an apiFetch error into merchant-friendly copy.
+ *
+ * Where possible we match on stable backend error codes rather than English
+ * message strings, so the mapping still works on localized sites. The two
+ * branches that still rely on message matches are flagged inline — they don't
+ * have stable codes to match against.
+ */
+export function friendlyEmailSendError( wpError: WPError ): string {
+ // apiFetch can reject with non-WPError shapes (native TypeError, wrapped middleware errors); unshaped errors fall through to the generic fallback.
+ const code = wpError?.code ?? '';
+ const message = wpError?.message ?? '';
+
+ // Covers both WP core (rest_cookie_invalid_nonce) and Woo's own
+ // EmailPreviewRestController check (invalid_nonce).
+ if ( code === 'rest_cookie_invalid_nonce' || code === 'invalid_nonce' ) {
+ return __(
+ 'Your session expired. Refresh the page and try again.',
+ 'woocommerce'
+ );
+ }
+
+ // Stable WP core code for a non-JSON response body.
+ if ( code === 'rest_invalid_json' ) {
+ return __(
+ 'The server returned unexpected output. Check your error log, or disable recently added plugins.',
+ 'woocommerce'
+ );
+ }
+
+ // Locale-fragile: WSOD responses don't carry a structured error code,
+ // so we fall back to matching the English phrase PHP prints.
+ if ( message.includes( 'critical error' ) ) {
+ return __(
+ 'A PHP error stopped the send. Check your error log or contact your host.',
+ 'woocommerce'
+ );
+ }
+
+ // Stable Woo code emitted by EmailPreviewRestController when the preview
+ // template fails to render.
+ if ( code === 'woocommerce_rest_email_preview_not_rendered' ) {
+ return __(
+ "The email couldn't be rendered. Try resetting the template in Settings → Emails.",
+ 'woocommerce'
+ );
+ }
+
+ // Locale-fragile: this apiFetch client fallback has no stable code, so
+ // we compare against the English message directly.
+ if ( message === 'Could not get a valid response from the server.' ) {
+ return __(
+ 'Your server timed out. If it keeps happening, ask your host to check PHP execution limits.',
+ 'woocommerce'
+ );
+ }
+
+ return __(
+ "Couldn't send the test email. Check your email settings and try again.",
+ 'woocommerce'
+ );
+}
+
export const EmailPreviewSend = ( { type }: EmailPreviewSendProps ) => {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const [ email, setEmail ] = useState( '' );
const [ isSending, setIsSending ] = useState( false );
const [ notice, setNotice ] = useState( '' );
const [ noticeType, setNoticeType ] = useState( '' );
+
const nonce = emailPreviewNonce();
const handleSendEmail = async () => {
setIsSending( true );
setNotice( '' );
+
try {
const response: EmailPreviewSendResponse = await apiFetch( {
path: `wc-admin-email/settings/email/send-preview?nonce=${ nonce }`,
method: 'POST',
data: { email, type },
} );
+
setNotice( response.message );
setNoticeType( 'success' );
+
recordEvent( 'settings_emails_preview_test_sent_successful', {
email_type: type,
} );
} catch ( e ) {
const wpError = e as WPError;
- setNotice( wpError.message );
+
+ setNotice( friendlyEmailSendError( wpError ) );
setNoticeType( 'error' );
+
recordEvent( 'settings_emails_preview_test_sent_failed', {
email_type: type,
error: wpError.message,
+ error_code: wpError.code,
} );
}
+
setIsSending( false );
};
@@ -96,6 +167,7 @@ export const EmailPreviewSend = ( { type }: EmailPreviewSendProps ) => {
placeholder={ __( 'Enter an email', 'woocommerce' ) }
onChange={ setEmail }
/>
+
{ notice && (
<div
className={ `wc-settings-email-preview-send-modal-notice wc-settings-email-preview-send-modal-notice-${ noticeType }` }
@@ -116,6 +188,7 @@ export const EmailPreviewSend = ( { type }: EmailPreviewSendProps ) => {
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
+
<Button
variant="primary"
onClick={ handleSendEmail }
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email.spec.ts
index f65b5e11cbb..9a572ceae9e 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email.spec.ts
+++ b/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email.spec.ts
@@ -201,9 +201,10 @@ test.describe( 'WooCommerce Email Settings', () => {
await expect( sendButton ).toBeEnabled();
await sendButton.click();
- // Wait for the message, because sending will fail in test environment
+ // Sending fails in the test env (no mail server); the backend returns
+ // `woocommerce_rest_email_preview_not_sent`, which hits the generic fallback in friendlyEmailSendError.
const message = modal.locator(
- 'text=Error sending test email. Please try again.'
+ "text=Couldn't send the test email. Check your email settings and try again."
);
await expect( message ).toBeVisible();
} );