diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index a0d9fd92983a0..7036bb00473df 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -965,50 +965,153 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; - /** - * Filters the template enhancement output buffer prior to sending to the client. - * - * This filter only applies the HTML output of an included template. This filter is a progressive enhancement - * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not - * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are - * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API - * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which - * fully supports HTML5. - * - * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any callbacks - * added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error: - * "Cannot use output buffering in output buffering display handlers." - * - * @since 6.9.0 - * - * @param string $filtered_output HTML template enhancement output buffer. - * @param string $output Original HTML template output buffer. - */ - $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + $did_just_catch = false; + + $error_log = array(); + set_error_handler( + static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log, &$did_just_catch ) { + // Switch a user error to an exception so that it can be caught and the buffer can be returned. + if ( E_USER_ERROR === $level ) { + throw new Exception( __( 'User error triggered:' ) . ' ' . $message ); + } + + // Display a caught exception as an error since it prevents any of the output buffer filters from applying. + if ( $did_just_catch ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.) + $level = E_USER_ERROR; + } + + // Capture a reported error to be displayed by appending to the processed output buffer if display_errors is enabled. + if ( error_reporting() & $level ) { + $error_log[] = compact( 'level', 'message', 'file', 'line' ); + } + return false; + } + ); + $original_display_errors = ini_get( 'display_errors' ); + if ( $original_display_errors ) { + ini_set( 'display_errors', 0 ); + } - /** - * Fires after the template enhancement output buffer has been finalized. - * - * This happens immediately before the template enhancement output buffer is flushed. No output may be printed at - * this action. However, HTTP headers may be sent, which makes this action complimentary to the - * {@see 'send_headers'} action, in which headers may be sent before the template has started rendering. In - * contrast, this `wp_finalized_template_enhancement_output_buffer` action is the possible point at which HTTP - * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started. This - * output buffer is automatically started if this action is added before - * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action with - * priority 1000. Before this point, the output buffer will also be started automatically if there was a - * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the - * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`. - * - * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks added - * to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error: - * "Cannot use output buffering in output buffering display handlers." - * - * @since 6.9.0 - * - * @param string $output Finalized output buffer. - */ - do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output ); + try { + /** + * Filters the template enhancement output buffer prior to sending to the client. + * + * This filter only applies the HTML output of an included template. This filter is a progressive enhancement + * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not + * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter + * are highly discouraged from using regular expressions to do any kind of replacement on the output. Use the + * HTML API (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of + * PHP 8.4 which fully supports HTML5. + * + * Do not print any output during this filter. While filters normally don't print anything, this is especially + * important since this applies during an output buffer callback. Prior to PHP 8.5, the output will be silently + * omitted, whereas afterward a deprecation notice will be emitted. + * + * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any + * callbacks added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a + * fatal error: "Cannot use output buffering in output buffering display handlers." + * + * @since 6.9.0 + * + * @param string $filtered_output HTML template enhancement output buffer. + * @param string $output Original HTML template output buffer. + */ + $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + } catch ( Throwable $throwable ) { + // Emit to the error log as a warning not as an error to prevent halting execution. + $did_just_catch = true; + trigger_error( + sprintf( + /* translators: %s is the throwable class name */ + __( 'Uncaught "%s" thrown:' ), + get_class( $throwable ) + ) . ' ' . $throwable->getMessage(), + E_USER_WARNING + ); + $did_just_catch = false; + } + + try { + /** + * Fires after the template enhancement output buffer has been finalized. + * + * This happens immediately before the template enhancement output buffer is flushed. No output may be printed + * at this action; prior to PHP 8.5, the output will be silently omitted, whereas afterward a deprecation notice + * will be emitted. Nevertheless, HTTP headers may be sent, which makes this action complimentary to the + * {@see 'send_headers'} action, in which headers may be sent before the template has started rendering. In + * contrast, this `wp_finalized_template_enhancement_output_buffer` action is the possible point at which HTTP + * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started. + * This output buffer is automatically started if this action is added before + * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action + * with priority 1000. Before this point, the output buffer will also be started automatically if there was a + * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the + * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`. + * + * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks + * added to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal + * error: "Cannot use output buffering in output buffering display handlers." + * + * @since 6.9.0 + * + * @param string $output Finalized output buffer. + */ + do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output ); + } catch ( Throwable $throwable ) { + // Emit to the error log as a warning not as an error to prevent halting execution. + $did_just_catch = true; + trigger_error( + sprintf( + /* translators: %s is the class name */ + __( 'Uncaught "%s" thrown:' ), + get_class( $throwable ) + ) . ' ' . $throwable->getMessage(), + E_USER_WARNING + ); + $did_just_catch = false; + } + + // Append any errors to be displayed before returning flushing the buffer. + if ( $original_display_errors && 'stderr' !== $original_display_errors ) { + foreach ( $error_log as $error ) { + switch ( $error['level'] ) { + case E_USER_NOTICE: + $type = 'Notice'; + break; + case E_USER_DEPRECATED: + $type = 'Deprecated'; + break; + case E_USER_WARNING: + $type = 'Warning'; + break; + default: + $type = 'Error'; + } + + if ( ini_get( 'html_errors' ) ) { + /* + * Adapted from PHP internals: . + * The self-closing tags are a vestige of the XHTML past! + */ + $format = "%s
\n%s: %s in %s on line %s
\n%s"; + } else { + // Adapted from PHP internals: . + $format = "%s\n%s: %s in %s on line %s\n%s"; + } + $filtered_output .= sprintf( + $format, + ini_get( 'error_prepend_string' ), + $type, + $error['message'], + $error['file'], + $error['line'], + ini_get( 'error_append_string' ) + ); + } + + ini_set( 'display_errors', $original_display_errors ); + } + + restore_error_handler(); return $filtered_output; } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index f3dd93ec4fb2b..e954f68b21923 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -63,11 +63,6 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); } - /** - * @var string - */ - protected $original_default_mimetype; - /** * @var WP_Scripts|null */ @@ -83,9 +78,28 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { */ protected $original_theme_features; + /** + * @var array + */ + const RESTORED_CONFIG_OPTIONS = array( + 'display_errors', + 'error_reporting', + 'log_errors', + 'error_log', + 'default_mimetype', + 'html_errors', + 'error_prepend_string', + 'error_append_string', + ); + + /** + * @var array + */ + protected $original_ini_config; + public function set_up() { parent::set_up(); - $this->original_default_mimetype = ini_get( 'default_mimetype' ); + register_post_type( 'cpt', array( @@ -117,6 +131,9 @@ public function set_up() { wp_styles(); $this->original_theme_features = $GLOBALS['_wp_theme_features']; + foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { + $this->original_ini_config[ $option ] = ini_get( $option ); + } } public function tear_down() { @@ -125,8 +142,10 @@ public function tear_down() { $wp_styles = $this->original_wp_styles; $GLOBALS['_wp_theme_features'] = $this->original_theme_features; + foreach ( $this->original_ini_config as $option => $value ) { + ini_set( $option, $value ); + } - ini_set( 'default_mimetype', $this->original_default_mimetype ); unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); @@ -977,6 +996,380 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi $this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_finalized_template_enhancement_output_buffer to be the same as the processed output buffer.' ); } + /** + * Data provider for data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing. + * + * @return array + */ + public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing(): array { + $log_and_display_all = array( + 'error_reporting' => E_ALL, + 'display_errors' => true, + 'log_errors' => true, + 'html_errors' => true, + ); + + $tests = array( + 'deprecated' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + ), + ), + 'notice' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + ), + 'warning' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + ), + 'error' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during filter?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'emit_action_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during action?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + ), + ), + 'exception' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + throw new Exception( 'I take exception to this filter!' ); + }, + 'emit_action_errors' => static function () { + throw new Exception( 'I take exception to this action!' ); + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Error: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + ), + ), + 'multiple_non_errors' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + ), + 'deprecated_without_html' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'html_errors' => false, + ) + ), + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + }, + 'emit_action_errors' => null, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + ), + ), + 'warning_in_eval_with_prepend_and_append' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'error_prepend_string' => '
PHP Problem!', + 'error_append_string' => '
', + ) + ), + 'emit_filter_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'emit_action_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', + ), + ), + 'notice_with_display_errors_stderr' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'display_errors' => 'stderr', + ) + ), + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.' ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.' ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array(), + ), + ); + + $tests_error_reporting_warnings_and_above = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['error_reporting'] = E_ALL ^ E_USER_NOTICE ^ E_USER_DEPRECATED; + + $test['expected_error_log'] = array_values( + array_filter( + $test['expected_error_log'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $test['expected_displayed_errors'] = array_values( + array_filter( + $test['expected_displayed_errors'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $tests_error_reporting_warnings_and_above[ "{$name}_with_warnings_and_above_reported" ] = $test; + } + + $tests_without_display_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['expected_displayed_errors'] = array(); + + $tests_without_display_errors[ "{$name}_without_display_errors" ] = $test; + } + + $tests_without_display_or_log_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['ini_config_options']['log_errors'] = false; + $test['expected_displayed_errors'] = array(); + $test['expected_error_log'] = array(); + + $tests_without_display_or_log_errors[ "{$name}_without_display_errors_or_log_errors" ] = $test; + } + + return array_merge( $tests, $tests_error_reporting_warnings_and_above, $tests_without_display_errors, $tests_without_display_or_log_errors ); + } + + /** + * Tests that errors are handled as expected when errors are emitted when filtering wp_template_enhancement_output_buffer or doing the wp_finalize_template_enhancement_output_buffer action. + * + * @ticket 43258 + * @ticket 64108 + * + * @covers ::wp_finalize_template_enhancement_output_buffer + * + * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing + */ + public function test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing( array $ini_config_options, ?Closure $emit_filter_errors, ?Closure $emit_action_errors, bool $expected_processed, array $expected_error_log, array $expected_displayed_errors ): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + ini_set( 'error_log', $this->temp_filename() ); // phpcs:ignore WordPress.PHP.IniSet.log_errors_Blacklisted, WordPress.PHP.IniSet.Risky + foreach ( $ini_config_options as $config => $option ) { + ini_set( $config, $option ); + } + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ) use ( $emit_filter_errors ): string { + $buffer = str_replace( 'Hello', 'Goodbye', $buffer ); + if ( $emit_filter_errors ) { + $emit_filter_errors(); + } + return $buffer; + } + ); + + if ( $emit_action_errors ) { + add_action( + 'wp_finalized_template_enhancement_output_buffer', + static function () use ( $emit_action_errors ): void { + $emit_action_errors(); + } + ); + } + + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + + ?> + + + + Greeting + + +

Hello World!

+ + + assertStringContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to have been processed.' ); + } else { + $this->assertStringNotContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to not have been processed.' ); + } + + $actual_error_log = array_values( + array_map( + static function ( string $error_log_entry ): string { + $error_log_entry = preg_replace( + '/^\[.+?] /', + '', + $error_log_entry + ); + $error_log_entry = preg_replace( + '#(?<= in ).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '__FILE__', + $error_log_entry + ); + return preg_replace( + '#(?<= on line )\d+#', + '__LINE__', + $error_log_entry + ); + }, + array_filter( explode( "\n", trim( file_get_contents( ini_get( 'error_log' ) ) ) ) ) + ) + ); + + $this->assertSame( + $expected_error_log, + $actual_error_log, + 'Expected same error log entries. Snapshot: ' . var_export( $actual_error_log, true ) + ); + + $displayed_errors = array_values( + array_map( + static function ( string $displayed_error ): string { + $displayed_error = str_replace( '
', '', $displayed_error ); + $displayed_error = preg_replace( + '#( in (?:)?).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '$1__FILE__', + $displayed_error + ); + return preg_replace( + '#( on line (?:)?)\d+#', + '$1__LINE__', + $displayed_error + ); + }, + array_filter( + explode( "\n", trim( $processed_output ) ), + static function ( $line ): bool { + return str_contains( $line, ' in ' ); + } + ) + ) + ); + + $this->assertSame( + $expected_displayed_errors, + $displayed_errors, + 'Expected the displayed errors to be the same. Snapshot: ' . var_export( $displayed_errors, true ) + ); + + if ( count( $expected_displayed_errors ) > 0 ) { + $this->assertStringEndsNotWith( '', rtrim( $processed_output ), 'Expected the output to have the error displayed.' ); + } else { + $this->assertStringEndsWith( '', rtrim( $processed_output ), 'Expected the output to not have the error displayed.' ); + } + } + /** * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes. *