From e158ff27de415fb57fab00e3d41ccddf44fa6b46 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 10 Oct 2015 09:05:04 +0000 Subject: [PATCH] Customizer: Fix scalability performance problem for previewing multidimensional settings. As the number of multidimensional settings (serialized options and theme mods) increase for a given ID base (e.g. a widget of a certain type), the number of calls to the `multidimensional` methods on `WP_Customize_Setting` increase exponentially, and the time for the preview to refresh grows in time exponentially as well. To improve performance, this change reduces the number of filters needed to preview the settings off of a multidimensional root from N to 1. This improves performance from `O(n^2)` to `O(n)`, but the linear increase is so low that the performance is essentially `O(1)` in comparison. This is achieved by introducing the concept of an "aggregated multidimensional" setting, where the root value of the multidimensional serialized setting value gets cached in a static array variable shared across all settings. Also improves performance by only adding preview filters if there is actually a need to do so: there is no need to add a filter if there is an initial value and if there is no posted value for a given setting (if it is not dirty). Fixes #32103. git-svn-id: https://develop.svn.wordpress.org/trunk@35007 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-customize-setting.php | 444 +++++++++++++----- tests/phpunit/tests/customize/setting.php | 28 +- 2 files changed, 344 insertions(+), 128 deletions(-) diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 6630157549..01b84c8e8b 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -81,6 +81,25 @@ class WP_Customize_Setting { */ protected $id_data = array(); + /** + * Cache of multidimensional values to improve performance. + * + * @since 4.4.0 + * @access protected + * @var array + * @static + */ + protected static $aggregated_multidimensionals = array(); + + /** + * Whether the multidimensional setting is aggregated. + * + * @since 4.4.0 + * @access protected + * @var bool + */ + protected $is_multidimensional_aggregated = false; + /** * Constructor. * @@ -96,27 +115,80 @@ class WP_Customize_Setting { public function __construct( $manager, $id, $args = array() ) { $keys = array_keys( get_object_vars( $this ) ); foreach ( $keys as $key ) { - if ( isset( $args[ $key ] ) ) + if ( isset( $args[ $key ] ) ) { $this->$key = $args[ $key ]; + } } $this->manager = $manager; $this->id = $id; // Parse the ID for array keys. - $this->id_data[ 'keys' ] = preg_split( '/\[/', str_replace( ']', '', $this->id ) ); - $this->id_data[ 'base' ] = array_shift( $this->id_data[ 'keys' ] ); + $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) ); + $this->id_data['base'] = array_shift( $this->id_data['keys'] ); // Rebuild the ID. $this->id = $this->id_data[ 'base' ]; - if ( ! empty( $this->id_data[ 'keys' ] ) ) - $this->id .= '[' . implode( '][', $this->id_data[ 'keys' ] ) . ']'; + if ( ! empty( $this->id_data[ 'keys' ] ) ) { + $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']'; + } - if ( $this->sanitize_callback ) + if ( $this->sanitize_callback ) { add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 ); - - if ( $this->sanitize_js_callback ) + } + if ( $this->sanitize_js_callback ) { add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 ); + } + + if ( 'option' === $this->type || 'theme_mod' === $this->type ) { + // Other setting types can opt-in to aggregate multidimensional explicitly. + $this->aggregate_multidimensional(); + } + } + + /** + * Get parsed ID data for multidimensional setting. + * + * @since 4.4.0 + * @access public + * + * @return array { + * ID data for multidimensional setting. + * + * @type string $base ID base + * @type array $keys Keys for multidimensional array. + * } + */ + final public function id_data() { + return $this->id_data; + } + + /** + * Set up the setting for aggregated multidimensional values. + * + * When a multidimensional setting gets aggregated, all of its preview and update + * calls get combined into one call, greatly improving performance. + * + * @since 4.4.0 + * @access protected + */ + protected function aggregate_multidimensional() { + if ( empty( $this->id_data['keys'] ) ) { + return; + } + + $id_base = $this->id_data['base']; + if ( ! isset( self::$aggregated_multidimensionals[ $this->type ] ) ) { + self::$aggregated_multidimensionals[ $this->type ] = array(); + } + if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) { + self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array( + 'previewed_instances' => array(), // Calling preview() will add the $setting to the array. + 'preview_applied_instances' => array(), // Flags for which settings have had their values applied. + 'root_value' => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls. + ); + } + $this->is_multidimensional_aggregated = true; } /** @@ -153,29 +225,76 @@ class WP_Customize_Setting { protected $_original_value; /** - * Set up filters for the setting so that the preview request - * will render the drafted changes. + * Add filters to supply the setting's value when accessed. + * + * If the setting already has a pre-existing value and there is no incoming + * post value for the setting, then this method will short-circuit since + * there is no change to preview. * * @since 3.4.0 + * @since 4.4.0 Added boolean return value. + * @access public + * + * @return bool False when preview short-circuits due no change needing to be previewed. */ public function preview() { - if ( ! isset( $this->_original_value ) ) { - $this->_original_value = $this->value(); - } if ( ! isset( $this->_previewed_blog_id ) ) { $this->_previewed_blog_id = get_current_blog_id(); } + $id_base = $this->id_data['base']; + $is_multidimensional = ! empty( $this->id_data['keys'] ); + $multidimensional_filter = array( $this, '_multidimensional_preview_filter' ); - switch( $this->type ) { + /* + * Check if the setting has a pre-existing value (an isset check), + * and if doesn't have any incoming post value. If both checks are true, + * then the preview short-circuits because there is nothing that needs + * to be previewed. + */ + $undefined = new stdClass(); + $needs_preview = ( $undefined !== $this->post_value( $undefined ) ); + $value = null; + + // Since no post value was defined, check if we have an initial value set. + if ( ! $needs_preview ) { + if ( $this->is_multidimensional_aggregated ) { + $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value']; + $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined ); + } else { + $default = $this->default; + $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set. + $value = $this->value(); + $this->default = $default; + } + $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied. + } + + if ( ! $needs_preview ) { + return false; + } + + switch ( $this->type ) { case 'theme_mod' : - add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) ); + if ( ! $is_multidimensional ) { + add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) ); + } else { + if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) { + // Only add this filter once for this ID base. + add_filter( "theme_mod_{$id_base}", $multidimensional_filter ); + } + self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this; + } break; case 'option' : - if ( empty( $this->id_data[ 'keys' ] ) ) - add_filter( 'pre_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) ); - else { - add_filter( 'option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) ); - add_filter( 'default_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) ); + if ( ! $is_multidimensional ) { + add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) ); + } else { + if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) { + // Only add these filters once for this ID base. + add_filter( "option_{$id_base}", $multidimensional_filter ); + add_filter( "default_option_{$id_base}", $multidimensional_filter ); + } + self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this; } break; default : @@ -204,17 +323,17 @@ class WP_Customize_Setting { */ do_action( "customize_preview_{$this->type}", $this ); } + return true; } /** - * Callback function to filter the theme mods and options. + * Callback function to filter non-multidimensional theme mods and options. * * If switch_to_blog() was called after the preview() method, and the current * blog is now not the same blog, then this method does a no-op and returns * the original value. * * @since 3.4.0 - * @uses WP_Customize_Setting::multidimensional_replace() * * @param mixed $original Old value. * @return mixed New or old value. @@ -224,15 +343,63 @@ class WP_Customize_Setting { return $original; } - $undefined = new stdClass(); // symbol hack + $undefined = new stdClass(); // Symbol hack. $post_value = $this->post_value( $undefined ); - if ( $undefined === $post_value ) { - $value = $this->_original_value; - } else { + if ( $undefined !== $post_value ) { $value = $post_value; + } else { + /* + * Note that we don't use $original here because preview() will + * not add the filter in the first place if it has an initial value + * and there is no post value. + */ + $value = $this->default; + } + return $value; + } + + /** + * Callback function to filter multidimensional theme mods and options. + * + * For all multidimensional settings of a given type, the preview filter for + * the first setting previewed will be used to apply the values for the others. + * + * @since 4.4.0 + * @access public + * + * @see WP_Customize_Setting::$aggregated_multidimensionals + * @param mixed $original Original root value. + * @return mixed New or old value. + */ + public function _multidimensional_preview_filter( $original ) { + if ( ! $this->is_current_blog_previewed() ) { + return $original; } - return $this->multidimensional_replace( $original, $this->id_data['keys'], $value ); + $id_base = $this->id_data['base']; + + // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value. + if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) { + return $original; + } + + foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) { + // Skip applying previewed value for any settings that have already been applied. + if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) { + continue; + } + + // Do the replacements of the posted/default sub value into the root value. + $value = $previewed_setting->post_value( $previewed_setting->default ); + $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value']; + $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value ); + self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root; + + // Mark this setting having been applied so that it will be skipped when the filter is called again. + self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true; + } + + return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value']; } /** @@ -298,6 +465,57 @@ class WP_Customize_Setting { return apply_filters( "customize_sanitize_{$this->id}", $value, $this ); } + /** + * Get the root value for a setting, especially for multidimensional ones. + * + * @since 4.4.0 + * @access protected + * + * @param mixed $default Value to return if root does not exist. + * @return mixed + */ + protected function get_root_value( $default = null ) { + $id_base = $this->id_data['base']; + if ( 'option' === $this->type ) { + return get_option( $id_base, $default ); + } else if ( 'theme_mod' ) { + return get_theme_mod( $id_base, $default ); + } else { + /* + * Any WP_Customize_Setting subclass implementing aggregate multidimensional + * will need to override this method to obtain the data from the appropriate + * location. + */ + return $default; + } + } + + /** + * Set the root value for a setting, especially for multidimensional ones. + * + * @since 4.4.0 + * @access protected + * + * @param mixed $value Value to set as root of multidimensional setting. + * @return bool Whether the multidimensional root was updated successfully. + */ + protected function set_root_value( $value ) { + $id_base = $this->id_data['base']; + if ( 'option' === $this->type ) { + return update_option( $id_base, $value ); + } else if ( 'theme_mod' ) { + set_theme_mod( $id_base, $value ); + return true; + } else { + /* + * Any WP_Customize_Setting subclass implementing aggregate multidimensional + * will need to override this method to obtain the data from the appropriate + * location. + */ + return false; + } + } + /** * Save the value of the setting, using the related API. * @@ -307,72 +525,52 @@ class WP_Customize_Setting { * @return bool The result of saving the value. */ protected function update( $value ) { - switch ( $this->type ) { - case 'theme_mod' : - $this->_update_theme_mod( $value ); - return true; + $id_base = $this->id_data['base']; + if ( 'option' === $this->type || 'theme_mod' === $this->type ) { + if ( ! $this->is_multidimensional_aggregated ) { + return $this->set_root_value( $value ); + } else { + $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value']; + $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value ); + self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root; + return $this->set_root_value( $root ); + } + } else { + /** + * Fires when the {@see WP_Customize_Setting::update()} method is called for settings + * not handled as theme_mods or options. + * + * The dynamic portion of the hook name, `$this->type`, refers to the type of setting. + * + * @since 3.4.0 + * + * @param mixed $value Value of the setting. + * @param WP_Customize_Setting $this WP_Customize_Setting instance. + */ + do_action( "customize_update_{$this->type}", $value, $this ); - case 'option' : - return $this->_update_option( $value ); - - default : - - /** - * Fires when the {@see WP_Customize_Setting::update()} method is called for settings - * not handled as theme_mods or options. - * - * The dynamic portion of the hook name, `$this->type`, refers to the type of setting. - * - * @since 3.4.0 - * - * @param mixed $value Value of the setting. - * @param WP_Customize_Setting $this WP_Customize_Setting instance. - */ - do_action( "customize_update_{$this->type}", $value, $this ); - - return has_action( "customize_update_{$this->type}" ); + return has_action( "customize_update_{$this->type}" ); } } /** - * Update the theme mod from the value of the parameter. + * Deprecated method. * * @since 3.4.0 - * - * @param mixed $value The value to update. + * @deprecated 4.4.0 Deprecated in favor of update() method. */ - protected function _update_theme_mod( $value ) { - // Handle non-array theme mod. - if ( empty( $this->id_data[ 'keys' ] ) ) { - set_theme_mod( $this->id_data[ 'base' ], $value ); - return; - } - // Handle array-based theme mod. - $mods = get_theme_mod( $this->id_data[ 'base' ] ); - $mods = $this->multidimensional_replace( $mods, $this->id_data[ 'keys' ], $value ); - if ( isset( $mods ) ) { - set_theme_mod( $this->id_data[ 'base' ], $mods ); - } + protected function _update_theme_mod() { + _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' ); } /** - * Update the option from the value of the setting. + * Deprecated method. * * @since 3.4.0 - * - * @param mixed $value The value to update. - * @return bool The result of saving the value. + * @deprecated 4.4.0 Deprecated in favor of update() method. */ - protected function _update_option( $value ) { - // Handle non-array option. - if ( empty( $this->id_data[ 'keys' ] ) ) - return update_option( $this->id_data[ 'base' ], $value ); - - // Handle array-based options. - $options = get_option( $this->id_data[ 'base' ] ); - $options = $this->multidimensional_replace( $options, $this->id_data[ 'keys' ], $value ); - if ( isset( $options ) ) - return update_option( $this->id_data[ 'base' ], $options ); + protected function _update_option() { + _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' ); } /** @@ -383,39 +581,33 @@ class WP_Customize_Setting { * @return mixed The value. */ public function value() { - // Get the callback that corresponds to the setting type. - switch( $this->type ) { - case 'theme_mod' : - $function = 'get_theme_mod'; - break; - case 'option' : - $function = 'get_option'; - break; - default : + $id_base = $this->id_data['base']; + $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type ); - /** - * Filter a Customize setting value not handled as a theme_mod or option. - * - * The dynamic portion of the hook name, `$this->id_date['base']`, refers to - * the base slug of the setting name. - * - * For settings handled as theme_mods or options, see those corresponding - * functions for available hooks. - * - * @since 3.4.0 - * - * @param mixed $default The setting default value. Default empty. - */ - return apply_filters( 'customize_value_' . $this->id_data[ 'base' ], $this->default ); + if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) { + $value = $this->get_root_value( $this->default ); + + /** + * Filter a Customize setting value not handled as a theme_mod or option. + * + * The dynamic portion of the hook name, `$this->id_date['base']`, refers to + * the base slug of the setting name. + * + * For settings handled as theme_mods or options, see those corresponding + * functions for available hooks. + * + * @since 3.4.0 + * + * @param mixed $default The setting default value. Default empty. + */ + $value = apply_filters( "customize_value_{$id_base}", $value ); + } else if ( $this->is_multidimensional_aggregated ) { + $root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value']; + $value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default ); + } else { + $value = $this->get_root_value( $this->default ); } - - // Handle non-array value - if ( empty( $this->id_data[ 'keys' ] ) ) - return $function( $this->id_data[ 'base' ], $this->default ); - - // Handle array-based value - $values = $function( $this->id_data[ 'base' ] ); - return $this->multidimensional_get( $values, $this->id_data[ 'keys' ], $this->default ); + return $value; } /** @@ -520,7 +712,7 @@ class WP_Customize_Setting { * @param $root * @param $keys * @param mixed $value The value to update. - * @return + * @return mixed */ final protected function multidimensional_replace( $root, $keys, $value ) { if ( ! isset( $value ) ) @@ -853,7 +1045,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } /** - * Get the instance data for a given widget setting. + * Get the instance data for a given nav_menu_item setting. * * @since 4.3.0 * @access public @@ -987,13 +1179,23 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { * Handle previewing the setting. * * @since 4.3.0 + * @since 4.4.0 Added boolean return value. * @access public * * @see WP_Customize_Manager::post_value() + * + * @return bool False if method short-circuited due to no-op. */ public function preview() { if ( $this->is_previewed ) { - return; + return false; + } + + $undefined = new stdClass(); + $is_placeholder = ( $this->post_id < 0 ); + $is_dirty = ( $undefined !== $this->post_value( $undefined ) ); + if ( ! $is_placeholder && ! $is_dirty ) { + return false; } $this->is_previewed = true; @@ -1009,6 +1211,8 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } // @todo Add get_post_metadata filters for plugins to add their data. + + return true; } /** @@ -1621,13 +1825,23 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { * Handle previewing the setting. * * @since 4.3.0 + * @since 4.4.0 Added boolean return value * @access public * * @see WP_Customize_Manager::post_value() + * + * @return bool False if method short-circuited due to no-op. */ public function preview() { if ( $this->is_previewed ) { - return; + return false; + } + + $undefined = new stdClass(); + $is_placeholder = ( $this->term_id < 0 ); + $is_dirty = ( $undefined !== $this->post_value( $undefined ) ); + if ( ! $is_placeholder && ! $is_dirty ) { + return false; } $this->is_previewed = true; @@ -1638,6 +1852,8 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 ); add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) ); add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) ); + + return true; } /** diff --git a/tests/phpunit/tests/customize/setting.php b/tests/phpunit/tests/customize/setting.php index 069a8ae1b0..600fb3faa9 100644 --- a/tests/phpunit/tests/customize/setting.php +++ b/tests/phpunit/tests/customize/setting.php @@ -102,7 +102,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview(), 'Preview should not no-op since setting has no existing value.' ); $this->assertEquals( $default, call_user_func( $type_options['getter'], $name, $this->undefined ), sprintf( 'Expected %s(%s) to return setting default: %s.', $type_options['getter'], $name, $default ) ); $this->assertEquals( $default, $setting->value() ); @@ -114,18 +114,18 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); + $this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' ); $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) ); $this->assertEquals( $initial_value, $setting->value() ); - // @todo What if we call the setter after preview() is called? If no post_value, should the new set value be stored? If that happens, then the following 3 assertions should be inverted $overridden_value = "overridden_value_$name"; call_user_func( $type_options['setter'], $name, $overridden_value ); - $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) ); - $this->assertEquals( $initial_value, $setting->value() ); - $this->assertNotEquals( $overridden_value, $setting->value() ); + $message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.'; + $this->assertEquals( $overridden_value, call_user_func( $type_options['getter'], $name ), $message ); + $this->assertEquals( $overridden_value, $setting->value(), $message ); + $this->assertNotEquals( $initial_value, $setting->value(), $message ); // Non-multidimensional: Test unset setting being overridden by a post value $name = "unset_{$type}_overridden"; @@ -133,7 +133,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); // activate post_data + $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() ); @@ -145,7 +145,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); // activate post_data + $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) ); @@ -167,7 +167,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertArrayHasKey( 'foo', $base_value ); $this->assertEquals( $default, $base_value['foo'] ); @@ -311,8 +311,8 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $initial_value, $setting->value() ); $setting->preview(); - $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) ); - $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) ); + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ), 'Zero preview actions because initial value is set with no incoming post value, so there is no preview to apply.' ); + $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above $this->assertEquals( $initial_value, $setting->value() ); // should be same as above @@ -325,8 +325,8 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); $setting->preview(); - $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) ); - $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) ); + $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ), 'One preview action now because initial value was not set and/or there is no incoming post value, so there is is a preview to apply.' ); + $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $post_data_overrides[ $name ], $setting->value() ); @@ -342,7 +342,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $initial_value, $setting->value() ); $setting->preview(); $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) ); - $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) ); + $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );