From dc5353a566a9de2e27cf8f5970647d4a6a58cbcd Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 26 Mar 2026 22:34:29 -0600 Subject: [PATCH 1/2] fix(tests): prevent Form_Manager_Test from killing PHPUnit process test_handle_model_delete_form_requires_confirmation called handle_model_delete_form() which invokes wp_send_json_error(). In non-AJAX context wp_send_json() falls back to a bare die statement that terminates the entire PHPUnit process (test 2533/4411 in #527). When wp_doing_ajax() returns true, wp_die() is used instead, but it routes through wp_die_ajax_handler (not wp_die_handler), so the test framework's WPDieException mechanism does not apply. Fix: install wp_doing_ajax and wp_die_ajax_handler filters inline in the test so that the AJAX die path throws WPAjaxDieContinueException (output was already sent by wp_send_json_error before wp_die is called). Filters are removed in a finally block to avoid leaking state. Closes #527 --- .../WP_Ultimo/Managers/Form_Manager_Test.php | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/WP_Ultimo/Managers/Form_Manager_Test.php b/tests/WP_Ultimo/Managers/Form_Manager_Test.php index 2a3eb2d5f..61914258e 100644 --- a/tests/WP_Ultimo/Managers/Form_Manager_Test.php +++ b/tests/WP_Ultimo/Managers/Form_Manager_Test.php @@ -99,6 +99,14 @@ public function test_get_form_url_returns_url(): void { /** * Test that handle_model_delete_form aborts when confirm is not set. * + * handle_model_delete_form() calls wp_send_json_error() which internally + * calls wp_die(). In AJAX context, wp_die() uses wp_die_ajax_handler + * (not wp_die_handler), so we must install both filters to prevent the + * bare die() call from killing the PHPUnit process (GitHub issue #527). + * + * wp_send_json_error() outputs JSON before calling wp_die(), so the AJAX + * die handler throws WPAjaxDieContinueException (output present). + * * @since 2.0.0 */ public function test_handle_model_delete_form_requires_confirmation(): void { @@ -110,8 +118,29 @@ public function test_handle_model_delete_form_requires_confirmation(): void { $_REQUEST['model'] = 'membership'; $_REQUEST['id'] = '1'; - $this->expectException(\WPDieException::class); - - $manager->handle_model_delete_form(); + /* + * Simulate AJAX context so wp_send_json_error() routes through + * wp_die() instead of a bare `die` statement. + * + * wp_die() in AJAX context uses wp_die_ajax_handler (not wp_die_handler). + * We install a handler that throws WPAjaxDieContinueException so PHPUnit + * can catch it instead of the process terminating. + */ + add_filter('wp_doing_ajax', '__return_true'); + $ajax_die_handler = function() { + return function( $message ) { + throw new \WPAjaxDieContinueException( (string) $message ); + }; + }; + add_filter('wp_die_ajax_handler', $ajax_die_handler, 1); + + try { + $this->expectException(\WPAjaxDieContinueException::class); + $manager->handle_model_delete_form(); + } finally { + remove_filter('wp_doing_ajax', '__return_true'); + remove_filter('wp_die_ajax_handler', $ajax_die_handler, 1); + unset($_REQUEST['model'], $_REQUEST['id']); + } } } From fc3f532db2ba357568f16531ec708c4f2e506730 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 26 Mar 2026 22:46:44 -0600 Subject: [PATCH 2/2] fix(tests): handle REST_REQUEST context and verify JSON response payload The previous commit used expectException() which was incompatible with the REST API context in CI (REST_REQUEST=true triggers a _doing_it_wrong notice from wp_send_json that the test framework treats as a failure). Switch to ob_start() + catch pattern: - Capture JSON output directly with ob_start() / ob_get_clean() - Catch WPAjaxDieContinueException explicitly instead of expectException - Assert the JSON payload (success=false, code=not-confirmed) - Conditionally call setExpectedIncorrectUsage('wp_send_json') only when REST_REQUEST is defined to handle both CI and local environments --- .../WP_Ultimo/Managers/Form_Manager_Test.php | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/WP_Ultimo/Managers/Form_Manager_Test.php b/tests/WP_Ultimo/Managers/Form_Manager_Test.php index 61914258e..bab6b1a63 100644 --- a/tests/WP_Ultimo/Managers/Form_Manager_Test.php +++ b/tests/WP_Ultimo/Managers/Form_Manager_Test.php @@ -105,7 +105,8 @@ public function test_get_form_url_returns_url(): void { * bare die() call from killing the PHPUnit process (GitHub issue #527). * * wp_send_json_error() outputs JSON before calling wp_die(), so the AJAX - * die handler throws WPAjaxDieContinueException (output present). + * die handler throws WPAjaxDieContinueException (output present). We + * capture the output with ob_start() and verify the JSON error payload. * * @since 2.0.0 */ @@ -118,6 +119,17 @@ public function test_handle_model_delete_form_requires_confirmation(): void { $_REQUEST['model'] = 'membership'; $_REQUEST['id'] = '1'; + /* + * wp_send_json() triggers a _doing_it_wrong notice when REST_REQUEST is + * defined (CI environment). Declare it as expected so the test framework + * does not treat it as a failure. Skip when REST_REQUEST is not defined + * (local environment) to avoid "Failed to assert that wp_send_json + * triggered an incorrect usage notice" errors. + */ + if (defined('REST_REQUEST') && REST_REQUEST) { + $this->setExpectedIncorrectUsage('wp_send_json'); + } + /* * Simulate AJAX context so wp_send_json_error() routes through * wp_die() instead of a bare `die` statement. @@ -134,13 +146,29 @@ public function test_handle_model_delete_form_requires_confirmation(): void { }; add_filter('wp_die_ajax_handler', $ajax_die_handler, 1); + $json_output = ''; + $exception_caught = false; + + ob_start(); + try { - $this->expectException(\WPAjaxDieContinueException::class); $manager->handle_model_delete_form(); - } finally { - remove_filter('wp_doing_ajax', '__return_true'); - remove_filter('wp_die_ajax_handler', $ajax_die_handler, 1); - unset($_REQUEST['model'], $_REQUEST['id']); + } catch (\WPAjaxDieContinueException $e) { + $exception_caught = true; } + + $json_output = ob_get_clean(); + + remove_filter('wp_doing_ajax', '__return_true'); + remove_filter('wp_die_ajax_handler', $ajax_die_handler, 1); + unset($_REQUEST['model'], $_REQUEST['id']); + + $this->assertTrue($exception_caught, 'handle_model_delete_form() should have terminated via wp_die()'); + + $response = json_decode($json_output, true); + + $this->assertIsArray($response, 'Response should be a JSON object'); + $this->assertFalse($response['success'], 'Response should indicate failure'); + $this->assertSame('not-confirmed', $response['data'][0]['code'], 'Error code should be not-confirmed'); } }