Make WordPress Core

Ticket #34893: 34893.7.diff

File 34893.7.diff, 32.0 KB (added by westonruter, 8 years ago)

https://github.com/xwp/wordpress-develop/pull/136/commits/0235decc780b1b155f1cfa8c1897f93ac70e18a6

  • src/wp-admin/css/customize-controls.css

    diff --git src/wp-admin/css/customize-controls.css src/wp-admin/css/customize-controls.css
    index 0a62611..47af403 100644
    p.customize-section-description { 
    494494.customize-control input[type="search"],
    495495.customize-control input[type="tel"],
    496496.customize-control input[type="url"] {
    497         width: 98%;
     497        width: 100%;
    498498        line-height: 18px;
    499499        margin: 0;
    500500}
    p.customize-section-description { 
    622622        border-right: 1px solid #ddd;
    623623}
    624624
     625
     626/**
     627 * Notifications
     628 */
     629
     630#customize-controls .customize-control-notifications-container { /* Scoped to #customize-controls for specificity over notification styles in common.css. */
     631        margin: 4px 0 8px 0;
     632        padding: 5px 0;
     633        display: none;
     634        cursor: default;
     635}
     636
     637.customize-control-widget_form.has-notifications .widget .widget-top,
     638.customize-control-nav_menu_item.has-notifications .menu-item-bar .menu-item-handle {
     639        background-color: #ffe0e0;
     640}
     641
     642.customize-control-notifications-container li {
     643        list-style: disc inside;
     644        margin-bottom: 0;
     645        margin-left: 0;
     646        border-left: solid 4px transparent;
     647        padding-left: 8px;
     648        padding-top: 4px;
     649        padding-bottom: 4px;
     650}
     651.customize-control-notifications-container li[data-type="error"] {
     652        border-left-color: #dc3232;
     653}
     654.customize-control-notifications-container li:only-child {
     655        list-style: none;
     656        margin-left: 0;
     657}
     658
     659
    625660/* Style for custom settings */
    626661
    627662/**
  • src/wp-admin/js/customize-controls.js

    diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
    index ef01674..b992d2c 100644
     
    2727                        this.id = id;
    2828                        this.transport = this.transport || 'refresh';
    2929                        this._dirty = options.dirty || false;
     30                        this.notifications = new api.Values({ defaultConstructor: api.Notification });
    3031
    3132                        // Whenever the setting's value changes, refresh the preview.
    3233                        this.bind( this.preview );
     
    14781479                        control.priority = new api.Value();
    14791480                        control.active = new api.Value();
    14801481                        control.activeArgumentsQueue = [];
     1482                        control.notifications = new api.Values({ defaultConstructor: api.Notification });
    14811483
    14821484                        control.elements = [];
    14831485
     
    15411543
    15421544                                        control.setting = control.settings['default'] || null;
    15431545
     1546                                        _.each( control.settings, function( setting ) {
     1547                                                setting.notifications.bind( 'add', function( settingNotification ) {
     1548                                                        var controlNotification = new api.Notification( setting.id + ':' + settingNotification.code, settingNotification );
     1549                                                        control.notifications.add( controlNotification.code, controlNotification );
     1550                                                } );
     1551                                                setting.notifications.bind( 'remove', function( settingNotification ) {
     1552                                                        control.notifications.remove( setting.id + ':' + settingNotification.code );
     1553                                                } );
     1554                                        } );
     1555
    15441556                                        control.embed();
    15451557                                }) );
    15461558                        }
    15471559
    15481560                        // After the control is embedded on the page, invoke the "ready" method.
    15491561                        control.deferred.embedded.done( function () {
     1562                                /*
     1563                                 * Note that this debounced/deferred rendering is needed for two reasons:
     1564                                 * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
     1565                                 * 2) Improve performance when adding/removing multiple notifications at a time.
     1566                                 */
     1567                                var debouncedRenderNotifications = _.debounce( function renderNotifications() {
     1568                                        control.renderNotifications();
     1569                                } );
     1570                                control.notifications.bind( 'add', debouncedRenderNotifications );
     1571                                control.notifications.bind( 'remove', debouncedRenderNotifications );
     1572                                control.renderNotifications();
     1573
    15501574                                control.ready();
    15511575                        });
    15521576                },
     
    15891613                ready: function() {},
    15901614
    15911615                /**
     1616                 * Get the element inside of a control's container that contains the validation error message.
     1617                 *
     1618                 * Control subclasses may override this to return the proper container to render notifications into.
     1619                 * Injects the notification container for existing controls that lack the necessary container,
     1620                 * including special handling for nav menu items and widgets.
     1621                 *
     1622                 * @returns {jQuery} Setting validation message element.
     1623                 * @this {wp.customize.Control}
     1624                 */
     1625                getNotificationsContainerElement: function() {
     1626                        var control = this, controlTitle, notificationsContainer;
     1627
     1628                        notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
     1629                        if ( notificationsContainer.length ) {
     1630                                return notificationsContainer;
     1631                        }
     1632
     1633                        notificationsContainer = $( '<div class="customize-control-notifications-container" aria-live="assertive"></div>' );
     1634
     1635                        if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
     1636                                control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
     1637                        } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
     1638                                control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
     1639                        } else {
     1640                                controlTitle = control.container.find( '.customize-control-title' );
     1641                                if ( controlTitle.length ) {
     1642                                        controlTitle.after( notificationsContainer );
     1643                                } else {
     1644                                        control.container.prepend( notificationsContainer );
     1645                                }
     1646                        }
     1647                        return notificationsContainer;
     1648                },
     1649
     1650                /**
     1651                 * Render notifications.
     1652                 *
     1653                 * Renders the `control.notifications` into the control's container.
     1654                 * Control subclasses may override this method to do their own handling
     1655                 * of rendering notifications.
     1656                 */
     1657                renderNotifications: function() {
     1658                        var control = this, container, notifications;
     1659                        container = control.getNotificationsContainerElement();
     1660                        if ( ! container || ! container.length ) {
     1661                                return;
     1662                        }
     1663                        notifications = [];
     1664                        control.notifications.each( function( notification ) {
     1665                                notifications.push( notification );
     1666                        } );
     1667
     1668                        if ( 0 === notifications.length ) {
     1669                                container.stop().slideUp( 'fast' );
     1670                        } else {
     1671                                container.stop().slideDown( 'fast', null, function() {
     1672                                        $( this ).css( 'height', 'auto' );
     1673                                } );
     1674                        }
     1675
     1676                        if ( ! control.notificationsTemplate ) {
     1677                                control.notificationsTemplate = wp.template( 'customize-control-notifications' );
     1678                        }
     1679
     1680                        control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
     1681                        container.empty().append( $.trim(
     1682                                control.notificationsTemplate( { notifications: notifications } )
     1683                        ) );
     1684                },
     1685
     1686                /**
    15921687                 * Normal controls do not expand, so just expand its parent
    15931688                 *
    15941689                 * @param {Object} [params]
     
    32233318                }
    32243319        });
    32253320
     3321        api.settingConstructor = {};
    32263322        api.controlConstructor = {
    32273323                color:         api.ColorControl,
    32283324                media:         api.MediaControl,
     
    33233419                                };
    33243420                        },
    33253421
     3422                        _handleInvalidSettingsError: function( response ) {
     3423                                var invalidControls = [], wasFocused = false;
     3424                                if ( ! response.invalid_settings || 0 === response.invalid_settings.length ) {
     3425                                        return;
     3426                                }
     3427
     3428                                // Find the controls that correspond to each invalid setting.
     3429                                _.each( response.invalid_settings, function( notifications, settingId ) {
     3430                                        var setting = api( settingId );
     3431                                        if ( setting ) {
     3432                                                _.each( notifications, function( notificationParams, code ) {
     3433                                                        var notification = new api.Notification( code, notificationParams );
     3434                                                        setting.notifications.add( code, notification );
     3435                                                } );
     3436                                        }
     3437
     3438                                        api.control.each( function( control ) {
     3439                                                _.each( control.settings, function( controlSetting ) {
     3440                                                        if ( controlSetting.id === settingId ) {
     3441                                                                invalidControls.push( control );
     3442                                                        }
     3443                                                } );
     3444                                        } );
     3445                                } );
     3446
     3447                                // Focus on the first control that is inside of an expanded section (one that is visible).
     3448                                _( invalidControls ).find( function( control ) {
     3449                                        var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
     3450                                        if ( isExpanded && control.expanded ) {
     3451                                                isExpanded = control.expanded();
     3452                                        }
     3453                                        if ( isExpanded ) {
     3454                                                control.focus();
     3455                                                wasFocused = true;
     3456                                        }
     3457                                        return wasFocused;
     3458                                } );
     3459
     3460                                // Focus on the first invalid control.
     3461                                if ( ! wasFocused && invalidControls[0] ) {
     3462                                        invalidControls[0].focus();
     3463                                }
     3464                        },
     3465
    33263466                        save: function() {
    33273467                                var self = this,
    33283468                                        processing = api.state( 'processing' ),
     
    33493489
    33503490                                        api.trigger( 'save', request );
    33513491
     3492                                        // Remove all setting notifications prior to save, allowing server to respond with new notifications.
     3493                                        api.each( function( setting ) {
     3494                                                setting.notifications.each( function( notification ) {
     3495                                                        setting.notifications.remove( notification.code );
     3496                                                } );
     3497                                        } );
     3498
    33523499                                        request.always( function () {
    33533500                                                body.removeClass( 'saving' );
    33543501                                                saveBtn.prop( 'disabled', false );
     
    33723519                                                                self.preview.iframe.show();
    33733520                                                        } );
    33743521                                                }
     3522
     3523                                                self._handleInvalidSettingsError( response );
     3524
    33753525                                                api.trigger( 'error', response );
    33763526                                        } );
    33773527
     
    34243574
    34253575                // Create Settings
    34263576                $.each( api.settings.settings, function( id, data ) {
    3427                         api.create( id, id, data.value, {
     3577                        var constructor = api.settingConstructor[ data.type ] || api.Setting,
     3578                                setting;
     3579
     3580                        setting = new constructor( id, data.value, {
    34283581                                transport: data.transport,
    34293582                                previewer: api.previewer,
    34303583                                dirty: !! data.dirty
    34313584                        } );
     3585                        api.add( id, setting );
    34323586                });
    34333587
    34343588                // Create Panels
  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index 0052a1a..b94c3ce 100644
    final class WP_Customize_Manager { 
    654654         * Return the sanitized value for a given setting from the request's POST data.
    655655         *
    656656         * @since 3.4.0
    657          * @since 4.1.1 Introduced 'default' parameter.
     657         * @since 4.1.1 Introduced `$default` parameter.
     658         * @since 4.6.0 Return `$default` when setting post value is invalid.
     659         * @see WP_REST_Server::dispatch()
     660         * @see WP_Rest_Request::sanitize_params()
     661         * @see WP_Rest_Request::has_valid_params()
    658662         *
    659          * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
    660          * @param mixed $default value returned $setting has no post value (added in 4.2.0).
    661          * @return string|mixed $post_value Sanitized value or the $default provided
     663         * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
     664         * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
     665         *                                      or the post value is invalid (added in 4.6.0).
     666         * @return string|mixed $post_value Sanitized value or the $default provided.
    662667         */
    663668        public function post_value( $setting, $default = null ) {
    664669                $post_values = $this->unsanitized_post_values();
    665                 if ( array_key_exists( $setting->id, $post_values ) ) {
    666                         return $setting->sanitize( $post_values[ $setting->id ] );
    667                 } else {
     670                if ( ! array_key_exists( $setting->id, $post_values ) ) {
     671                        return $default;
     672                }
     673                $value = $setting->sanitize( $post_values[ $setting->id ] );
     674                if ( is_null( $value ) || is_wp_error( $value ) ) {
     675                        return $default;
     676                }
     677                $valid = $setting->validate( $value );
     678                if ( is_wp_error( $valid ) ) {
    668679                        return $default;
    669680                }
     681                return $value;
    670682        }
    671683
    672684        /**
    final class WP_Customize_Manager { 
    970982        }
    971983
    972984        /**
     985         * Validate setting values.
     986         *
     987         * Sanitization is applied to the values before being passed for validation.
     988         * Validation is skipped for unregistered settings or for values that are
     989         * already null since they will be skipped anyway.
     990         *
     991         * @since 4.6.0
     992         * @access public
     993         * @see WP_REST_Request::has_valid_params()
     994         *
     995         * @param array $setting_values Mapping of setting IDs to values to sanitize and validate.
     996         * @return array Empty array if all settings were valid. One or more instances of `WP_Error` if any were invalid.
     997         */
     998        public function validate_setting_values( $setting_values ) {
     999                $validity_errors = array();
     1000                foreach ( $setting_values as $setting_id => $unsanitized_value ) {
     1001                        $setting = $this->get_setting( $setting_id );
     1002                        if ( ! $setting || is_null( $unsanitized_value ) ) {
     1003                                continue;
     1004                        }
     1005                        $validity = $setting->validate( $setting->sanitize( $unsanitized_value ) );
     1006                        if ( false === $validity || null === $validity ) {
     1007                                $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
     1008                        }
     1009                        if ( is_wp_error( $validity ) ) {
     1010                                $validity_errors[ $setting_id ] = $validity;
     1011                        }
     1012                }
     1013                return $validity_errors;
     1014        }
     1015
     1016        /**
    9731017         * Switch the theme and trigger the save() method on each setting.
    9741018         *
    9751019         * @since 3.4.0
    final class WP_Customize_Manager { 
    9841028                        wp_send_json_error( 'invalid_nonce' );
    9851029                }
    9861030
     1031                /**
     1032                 * Fires before save validation happens.
     1033                 *
     1034                 * Plugins can add just-in-time `customize_validate_{$setting_id}` filters
     1035                 * at this point to catch any settings registered after `customize_register`.
     1036                 *
     1037                 * @since 4.6.0
     1038                 *
     1039                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     1040                 */
     1041                do_action( 'customize_save_validation_before', $this );
     1042
     1043                // Validate settings.
     1044                $validity_errors = $this->validate_setting_values( $this->unsanitized_post_values() );
     1045                $invalid_count = count( $validity_errors );
     1046                if ( $invalid_count > 0 ) {
     1047                        $settings_errors = array();
     1048                        foreach ( $validity_errors as $setting_id => $validity_error ) {
     1049                                $settings_errors[ $setting_id ] = array();
     1050                                foreach ( $validity_error->errors as $error_code => $error_message ) {
     1051                                        $settings_errors[ $setting_id ][ $error_code ] = array(
     1052                                                'message' => join( ' ', $error_message ),
     1053                                                'data' => $validity_error->get_error_data( $error_code ),
     1054                                        );
     1055                                }
     1056                        }
     1057                        $response = array(
     1058                                'invalid_settings' => $settings_errors,
     1059                                'message' => sprintf( _n( 'There is %d invalid setting.', 'There are %d invalid settings.', $invalid_count ), $invalid_count ),
     1060                        );
     1061
     1062                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
     1063                        $response = apply_filters( 'customize_save_response', $response, $this );
     1064                        wp_send_json_error( $response );
     1065                }
     1066
    9871067                // Do we have to switch themes?
    9881068                if ( ! $this->is_theme_active() ) {
    9891069                        // Temporarily stop previewing the theme to allow switch_themes()
    final class WP_Customize_Manager { 
    14031483                        ) );
    14041484                        $control->print_template();
    14051485                }
     1486                ?>
     1487                <script type="text/html" id="tmpl-customize-control-notifications">
     1488                        <ul>
     1489                                <# _.each( data.notifications, function( notification ) { #>
     1490                                        <li data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{ notification.message || notification.code }}</li>
     1491                                <# } ); #>
     1492                        </ul>
     1493                </script>
     1494                <?php
    14061495        }
    14071496
    14081497        /**
    final class WP_Customize_Manager { 
    17631852                                        printf(
    17641853                                                "s[%s] = %s;\n",
    17651854                                                wp_json_encode( $setting->id ),
    1766                                                 wp_json_encode( array(
    1767                                                         'value'     => $setting->js_value(),
    1768                                                         'transport' => $setting->transport,
    1769                                                         'dirty'     => $setting->dirty,
    1770                                                 ) )
     1855                                                wp_json_encode( $setting->json() )
    17711856                                        );
    17721857                                }
    17731858                        }
  • src/wp-includes/class-wp-customize-setting.php

    diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
    index 94e7ded..9679883 100644
    class WP_Customize_Setting { 
    5959         *
    6060         * @var callback
    6161         */
     62        public $validate_callback    = '';
    6263        public $sanitize_callback    = '';
    6364        public $sanitize_js_callback = '';
    6465
    class WP_Customize_Setting { 
    142143                        $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
    143144                }
    144145
     146                if ( $this->validate_callback ) {
     147                        add_filter( "customize_validate_{$this->id}", $this->validate_callback, 10, 3 );
     148                }
    145149                if ( $this->sanitize_callback ) {
    146                         add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
     150                        add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 3 );
    147151                }
    148152                if ( $this->sanitize_js_callback ) {
    149153                        add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
    class WP_Customize_Setting { 
    464468         * the value of the setting.
    465469         *
    466470         * @since 3.4.0
     471         * @since 4.6.0 Return the result of updating the value.
    467472         *
    468          * @return false|void False if cap check fails or value isn't set.
     473         * @return false|void False if cap check fails or value isn't set or is invalid.
    469474         */
    470475        final public function save() {
    471476                $value = $this->post_value();
    472477
    473                 if ( ! $this->check_capabilities() || ! isset( $value ) )
     478                if ( ! $this->check_capabilities() || ! isset( $value ) ) {
    474479                        return false;
     480                }
    475481
    476482                /**
    477483                 * Fires when the WP_Customize_Setting::save() method is called.
    class WP_Customize_Setting { 
    483489                 *
    484490                 * @param WP_Customize_Setting $this WP_Customize_Setting instance.
    485491                 */
    486                 do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
     492                do_action( 'customize_save_' . $this->id_data['base'], $this );
    487493
    488494                $this->update( $value );
    489495        }
    class WP_Customize_Setting { 
    494500         * @since 3.4.0
    495501         *
    496502         * @param mixed $default A default value which is used as a fallback. Default is null.
    497          * @return mixed The default value on failure, otherwise the sanitized value.
     503         * @return mixed The default value on failure, otherwise the sanitized and validated value.
    498504         */
    499505        final public function post_value( $default = null ) {
    500506                return $this->manager->post_value( $this, $default );
    class WP_Customize_Setting { 
    505511         *
    506512         * @since 3.4.0
    507513         *
    508          * @param string|array $value The value to sanitize.
    509          * @return string|array|null Null if an input isn't valid, otherwise the sanitized value.
     514         * @param string|array $value    The value to sanitize.
     515         * @return string|array|null|WP_Error Sanitized value, or `null`/`WP_Error` if invalid.
    510516         */
    511517        public function sanitize( $value ) {
    512518
    class WP_Customize_Setting { 
    522528        }
    523529
    524530        /**
     531         * Validate an input.
     532         *
     533         * @since 4.6.0
     534         * @access public
     535         * @see WP_REST_Request::has_valid_params()
     536         *
     537         * @param mixed $value Value to validate.
     538         * @return true|WP_Error
     539         */
     540        public function validate( $value ) {
     541                if ( is_wp_error( $value ) ) {
     542                        return $value;
     543                }
     544                if ( is_null( $value ) ) {
     545                        return new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
     546                }
     547
     548                $validity = new WP_Error();
     549
     550                /**
     551                 * Validate a Customize setting value.
     552                 *
     553                 * Plugins should amend the `$validity` object via its `WP_Error::add()` method.
     554                 *
     555                 * @since 4.6.0
     556                 *
     557                 * @param WP_Error             $validity Filtered from `true` to `WP_Error` when invalid.
     558                 * @param mixed                $value    Value of the setting.
     559                 * @param WP_Customize_Setting $this     WP_Customize_Setting instance.
     560                 */
     561                $validity = apply_filters( "customize_validate_{$this->id}", $validity, $value, $this );
     562
     563                if ( is_wp_error( $validity ) && empty( $validity->errors ) ) {
     564                        $validity = true;
     565                }
     566                return $validity;
     567        }
     568
     569        /**
    525570         * Get the root value for a setting, especially for multidimensional ones.
    526571         *
    527572         * @since 4.4.0
    class WP_Customize_Setting { 
    700745        }
    701746
    702747        /**
     748         * Get the data to export to the client via JSON.
     749         *
     750         * @since 4.6.0
     751         *
     752         * @return array Array of parameters passed to JavaScript.
     753         */
     754        public function json() {
     755                return array(
     756                        'value'     => $this->js_value(),
     757                        'transport' => $this->transport,
     758                        'dirty'     => $this->dirty,
     759                        'type'      => $this->type,
     760                );
     761        }
     762
     763        /**
    703764         * Validate user capabilities whether the theme supports the setting.
    704765         *
    705766         * @since 3.4.0
  • src/wp-includes/js/customize-base.js

    diff --git src/wp-includes/js/customize-base.js src/wp-includes/js/customize-base.js
    index b4b7279..eb7fa44 100644
    window.wp = window.wp || {}; 
    755755        // Add the Events mixin to api.Messenger.
    756756        $.extend( api.Messenger.prototype, api.Events );
    757757
     758        /**
     759         * Notification.
     760         *
     761         * @class
     762         * @augments wp.customize.Class
     763         *
     764         * @param {string} code                The error code.
     765         * @param {object} params              Params.
     766         * @param {string} params.message      The error message.
     767         * @param {string} [params.type=error] The notification type.
     768         * @param {*}      [params.data]       Any additional data.
     769         */
     770        api.Notification = api.Class.extend({
     771                initialize: function( code, params ) {
     772                        this.code = code;
     773                        this.message = params.message;
     774                        this.type = params.type || 'error';
     775                        this.data = params.data || null;
     776                }
     777        });
     778
    758779        // The main API object is also a collection of all customizer settings.
    759780        api = $.extend( new api.Values(), api );
    760781
  • tests/phpunit/tests/customize/setting.php

    diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
    index c2ff7f5..98990c5 100644
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    4242                $this->assertEquals( 'refresh', $setting->transport );
    4343                $this->assertEquals( '', $setting->sanitize_callback );
    4444                $this->assertEquals( '', $setting->sanitize_js_callback );
     45                $this->assertFalse( has_filter( "customize_validate_{$setting->id}" ) );
    4546                $this->assertFalse( has_filter( "customize_sanitize_{$setting->id}" ) );
    4647                $this->assertFalse( has_filter( "customize_sanitize_js_{$setting->id}" ) );
    4748                $this->assertEquals( false, $setting->dirty );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    5455                        'theme_supports' => 'widgets',
    5556                        'default' => 'barbar',
    5657                        'transport' => 'postMessage',
     58                        'validate_callback' => create_function( '$value', 'return $value . ":validate_callback";' ),
    5759                        'sanitize_callback' => create_function( '$value', 'return $value . ":sanitize_callback";' ),
    5860                        'sanitize_js_callback' => create_function( '$value', 'return $value . ":sanitize_js_callback";' ),
    5961                );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    6264                foreach ( $args as $key => $value ) {
    6365                        $this->assertEquals( $value, $setting->$key );
    6466                }
     67                $this->assertEquals( 10, has_filter( "customize_validate_{$setting->id}", $args['validate_callback'] ) );
    6568                $this->assertEquals( 10, has_filter( "customize_sanitize_{$setting->id}", $args['sanitize_callback'] ) );
    6669                $this->assertEquals( 10, has_filter( "customize_sanitize_js_{$setting->id}" ), $args['sanitize_js_callback'] );
    6770        }
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    9093
    9194        /**
    9295         * Run assertions on non-multidimensional standard settings.
     96         *
     97         * @see WP_Customize_Setting::value()
    9398         */
    9499        function test_preview_standard_types_non_multidimensional() {
    95100                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    167172         * Run assertions on multidimensional standard settings.
    168173         *
    169174         * @see WP_Customize_Setting::preview()
     175         * @see WP_Customize_Setting::value()
    170176         */
    171177        function test_preview_standard_types_multidimensional() {
    172178                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    569575                $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $id_base ) );
    570576                $this->assertEquals( 'no', $autoload, 'Even though setting1 did not indicate autoload (thus normally true), since another multidimensional option setting of the base did say autoload=false, it should be autoload=no' );
    571577        }
     578
     579        /**
     580         * Test js_value and json methods.
     581         *
     582         * @see WP_Customize_Setting::js_value()
     583         * @see WP_Customize_Setting::json()
     584         */
     585        public function test_js_value() {
     586                $default = "\x00";
     587                $args = array(
     588                        'type' => 'binary',
     589                        'default' => $default,
     590                        'transport' => 'postMessage',
     591                        'dirty' => true,
     592                        'sanitize_js_callback' => create_function( '$value', 'return base64_encode( $value );' ),
     593                );
     594                $setting = new WP_Customize_Setting( $this->manager, 'name', $args );
     595
     596                $this->assertEquals( $default, $setting->value() );
     597                $this->assertEquals( base64_encode( $default ), $setting->js_value() );
     598
     599                $exported = $setting->json();
     600                $this->assertArrayHasKey( 'type', $exported );
     601                $this->assertArrayHasKey( 'value', $exported );
     602                $this->assertArrayHasKey( 'transport', $exported );
     603                $this->assertArrayHasKey( 'dirty', $exported );
     604                $this->assertEquals( $setting->js_value(), $exported['value'] );
     605                $this->assertEquals( $args['type'], $setting->type );
     606                $this->assertEquals( $args['transport'], $setting->transport );
     607                $this->assertEquals( $args['dirty'], $setting->dirty );
     608        }
     609
     610        /**
     611         * Test validate.
     612         *
     613         * @see WP_Customize_Setting::validate()
     614         */
     615        public function test_validate() {
     616                $setting = new WP_Customize_Setting( $this->manager, 'name', array(
     617                        'type' => 'key',
     618                        'validate_callback' => array( $this, 'filter_validate_for_test_validate' ),
     619                ) );
     620                $validity = $setting->validate( 'BAD!' );
     621                $this->assertInstanceOf( 'WP_Error', $validity );
     622                $this->assertEquals( 'invalid_key', $validity->get_error_code() );
     623        }
     624
     625        /**
     626         * Validate callback.
     627         *
     628         * @see Tests_WP_Customize_Setting::test_validate()
     629         *
     630         * @param WP_Error $validity Validity.
     631         * @param string   $value    Value.
     632         *
     633         * @return WP_Error
     634         */
     635        public function filter_validate_for_test_validate( $validity, $value ) {
     636                $this->assertInstanceOf( 'WP_Error', $validity );
     637                $this->assertInternalType( 'string', $value );
     638                if ( sanitize_key( $value ) !== $value ) {
     639                        $validity->add( 'invalid_key', 'Invalid key' );
     640                }
     641                return $validity;
     642        }
    572643}
    573644
  • tests/qunit/fixtures/customize-settings.js

    diff --git tests/qunit/fixtures/customize-settings.js tests/qunit/fixtures/customize-settings.js
    index 85bd1ab..670f783 100644
    window._wpCustomizeSettings = { 
    112112                'fixture-setting': {
    113113                        'transport': 'postMessage',
    114114                        'value': 'Lorem Ipsum'
     115                },
     116                'fixture-setting-abbr': {
     117                        'transport': 'postMessage',
     118                        'value': 'NASA',
     119                        'type': 'abbreviation'
    115120                }
    116121        },
    117122        'theme': {
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index aa454a9..d9739fa 100644
     
    126126                                <# } #>
    127127                        </li>
    128128                </script>
     129                <script type="text/html" id="tmpl-customize-control-notifications">
     130                        <ul>
     131                                <# _.each( data.notifications, function( notification ) { #>
     132                                        <li data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{ notification.message || notification.code }}</li>
     133                                <# } ); #>
     134                        </ul>
     135                </script>
    129136
    130137                <!-- Templates for Customizer Menus -->
    131138                <script type="text/html" id="tmpl-customize-control-nav_menu-content">
  • tests/qunit/wp-admin/js/customize-base.js

    diff --git tests/qunit/wp-admin/js/customize-base.js tests/qunit/wp-admin/js/customize-base.js
    index c19ea80..b7aca0d 100644
     
    1 /* global wp */
     1/* global wp, test, ok, equal, module */
    22
    33jQuery( function( $ ) {
    44        var FooSuperClass, BarSubClass, foo, bar, ConstructorTestClass, newConstructor, constructorTest, $mockElement, mockString,
    jQuery( function( $ ) { 
    158158                firstValueInstance.set( 'newValue' );
    159159                ok( wasCallbackFired );
    160160        });
     161
     162        module( 'Customize Base: Notification' );
     163        test( 'Notification object exists and has expected properties', function ( assert ) {
     164                var notification = new wp.customize.Notification( 'mycode', {
     165                        'message': 'Hello World',
     166                        'type': 'update',
     167                        'data': { 'foo': 'bar' }
     168                } );
     169
     170                assert.equal( 'mycode', notification.code );
     171                assert.equal( 'Hello World', notification.message );
     172                assert.equal( 'update', notification.type );
     173                assert.deepEqual( { 'foo': 'bar' }, notification.data );
     174
     175                notification = new wp.customize.Notification( 'mycode2', {
     176                        'message': 'Hello Space'
     177                } );
     178                assert.equal( 'mycode2', notification.code );
     179                assert.equal( 'Hello Space', notification.message );
     180                assert.equal( 'error', notification.type );
     181                assert.equal( null, notification.data );
     182        } );
    161183});
  • tests/qunit/wp-admin/js/customize-controls.js

    diff --git tests/qunit/wp-admin/js/customize-controls.js tests/qunit/wp-admin/js/customize-controls.js
    index 47d695c..23ca9ac 100644
     
    1 /* global wp */
     1/* global wp, test, ok, equal, module */
     2
     3wp.customize.settingConstructor.abbreviation = wp.customize.Setting.extend({
     4        validate: function( value ) {
     5                return value.toUpperCase();
     6        }
     7});
    28
    39jQuery( window ).load( function (){
    410        'use strict';
    jQuery( window ).load( function (){ 
    8591        test( 'Setting has fixture value', function () {
    8692                equal( wp.customize( 'fixture-setting' )(), 'Lorem Ipsum' );
    8793        } );
     94        test( 'Setting has notifications', function () {
     95                var setting = wp.customize( 'fixture-setting' );
     96                ok( setting.notifications.extended( wp.customize.Values ) );
     97                equal( wp.customize.Notification, setting.notifications.prototype.constructor.defaultConstructor );
     98        } );
     99        test( 'Setting constructor object exists', function( assert ) {
     100                assert.ok( _.isObject( wp.customize.settingConstructor ) );
     101        } );
     102        test( 'Custom setting constructor is used', function( assert ) {
     103                var setting = wp.customize( 'fixture-setting-abbr' );
     104                assert.ok( setting.extended( wp.customize.settingConstructor.abbreviation ) );
     105                setting.set( 'usa' );
     106                assert.equal( 'USA', setting.get() );
     107        } );
    88108
    89109        module( 'Customizer Control in Fixture' );
    90110        test( 'Control exists', function () {
    jQuery( window ).load( function (){ 
    99119                var control = wp.customize.control( 'fixture-control' );
    100120                equal( control.section(), 'fixture-section' );
    101121        } );
     122        test( 'Control has notifications', function ( assert ) {
     123                var control = wp.customize.control( 'fixture-control' ), settingNotification, controlOnlyNotification, doneEmbedded;
     124                assert.ok( control.notifications.extended( wp.customize.Values ) );
     125                assert.equal( wp.customize.Notification, control.notifications.prototype.constructor.defaultConstructor );
     126                assert.ok( _.isFunction( control.getNotificationsContainerElement ) );
     127                assert.ok( _.isFunction( control.renderNotifications ) );
     128
     129                doneEmbedded = assert.async();
     130                control.deferred.embedded.done( function() {
     131                        var notificationContainerElement;
     132
     133                        assert.equal( 0, _.size( control.notifications._value ) );
     134                        assert.equal( 0, _.size( control.settings['default'].notifications._value ) );
     135
     136                        notificationContainerElement = control.getNotificationsContainerElement();
     137                        assert.equal( 1, notificationContainerElement.length );
     138                        assert.ok( notificationContainerElement.is( '.customize-control-notifications-container' ) );
     139                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
     140                        assert.equal( 'none', notificationContainerElement.css( 'display' ) );
     141
     142                        settingNotification = new wp.customize.Notification( 'setting_invalidity', 'Invalid setting' );
     143                        controlOnlyNotification = new wp.customize.Notification( 'control_invalidity', 'Invalid control' );
     144                        control.settings['default'].notifications.add( settingNotification.code, settingNotification );
     145                        control.notifications.add( controlOnlyNotification.code, controlOnlyNotification );
     146
     147                        // Note that renderNotifications is being called manually here since rendering normally happens asynchronously.
     148                        control.renderNotifications();
     149
     150                        assert.equal( 2, notificationContainerElement.find( '> ul > li' ).length );
     151                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
     152                        assert.equal( 2, _.size( control.notifications._value ) );
     153                        assert.equal( 1, _.size( control.settings['default'].notifications._value ) );
     154
     155                        control.notifications.remove( controlOnlyNotification.code );
     156                        control.renderNotifications();
     157                        assert.equal( 1, notificationContainerElement.find( '> ul > li' ).length );
     158                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
     159
     160                        control.settings['default'].notifications.remove( settingNotification.code );
     161                        control.renderNotifications();
     162                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
     163                        assert.ok( notificationContainerElement.is( ':animated' ) ); // It is being slid down.
     164                        notificationContainerElement.stop().hide(); // Clean up.
     165
     166                        doneEmbedded();
     167                } );
     168        } );
    102169
    103170        module( 'Customizer control without associated settings' );
    104171        test( 'Control can be created without settings', function() {