<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[37700] trunk: Customize: Update server-sent setting validation notifications as changes are entered.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/37700">37700</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/37700","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-06-14 19:16:54 +0000 (Tue, 14 Jun 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Customize: Update server-sent setting validation notifications as changes are entered.

Send back setting validities with full refreshes and selective refreshes so that invalid settings can have notifications displayed immediately before attempting save, and so that these notifications can be cleared as soon as the input is corrected.

* Splits out JS logic for listing controls into separate methods  `wp.customize.Setting.prototype.findControls()` and `wp.customize.findControlsForSettings()`.
* Adds a `setting` property to the `data` on notifications added to controls that are synced from their settings.
* Adds `selective-refresh-setting-validities` message sent from preview to pane.
* Changes `WP_Customize_Manager::validate_setting_values()` to return when settings are valid as well as invalid.
* Adds `WP_Customize_Manager::prepare_setting_validity_for_js()`.
* Add setting validities to data exported to JS in Customizer Preview and in selective refresh responses.

Fixes <a href="https://core.trac.wordpress.org/ticket/36944">#36944</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminjscustomizecontrolsjs">trunk/src/wp-admin/js/customize-controls.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludescustomizeclasswpcustomizeselectiverefreshphp">trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewjs">trunk/src/wp-includes/js/customize-preview.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizeselectiverefreshjs">trunk/src/wp-includes/js/customize-selective-refresh.js</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
<li><a href="#trunktestsphpunittestscustomizeselectiverefreshajaxphp">trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php</a></li>
<li><a href="#trunktestsqunitwpadminjscustomizecontrolsjs">trunk/tests/qunit/wp-admin/js/customize-controls.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-controls.js       2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/src/wp-admin/js/customize-controls.js 2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -43,6 +43,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                case 'postMessage':
</span><span class="cx" style="display: block; padding: 0 10px">                                        return this.previewer.send( 'setting', [ this.id, this() ] );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                },
+
+               /**
+                * Find controls associated with this setting.
+                *
+                * @since 4.6.0
+                * @returns {wp.customize.Control[]} Controls associated with setting.
+                */
+               findControls: function() {
+                       var setting = this, controls = [];
+                       api.control.each( function( control ) {
+                               _.each( control.settings, function( controlSetting ) {
+                                       if ( controlSetting.id === setting.id ) {
+                                               controls.push( control );
+                                       }
+                               } );
+                       } );
+                       return controls;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1543,9 +1561,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        control.setting = control.settings['default'] || null;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        // Add setting notifications to the control notification.
</ins><span class="cx" style="display: block; padding: 0 10px">                                         _.each( control.settings, function( setting ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                setting.notifications.bind( 'add', function( settingNotification ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        var controlNotification = new api.Notification( setting.id + ':' + settingNotification.code, settingNotification );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 var controlNotification, code, params;
+                                                       code = setting.id + ':' + settingNotification.code;
+                                                       params = _.extend(
+                                                               {},
+                                                               settingNotification,
+                                                               {
+                                                                       setting: setting.id
+                                                               }
+                                                       );
+                                                       controlNotification = new api.Notification( code, params );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                         control.notifications.add( controlNotification.code, controlNotification );
</span><span class="cx" style="display: block; padding: 0 10px">                                                } );
</span><span class="cx" style="display: block; padding: 0 10px">                                                setting.notifications.bind( 'remove', function( settingNotification ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2908,6 +2936,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                }
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><span class="cx" style="display: block; padding: 0 10px">                                } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               if ( data.settingValidities ) {
+                                       api._handleSettingValidities( {
+                                               settingValidities: data.settingValidities,
+                                               focusInvalidControl: false
+                                       } );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                         } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        this.request = $.ajax( this.previewUrl(), {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3430,68 +3465,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                };
</span><span class="cx" style="display: block; padding: 0 10px">                        },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        /**
-                        * Handle invalid_settings in an error response for the customize-save request.
-                        *
-                        * Add notifications to the settings and focus on the first control that has an invalid setting.
-                        *
-                        * @since 4.6.0
-                        * @private
-                        *
-                        * @param {object} response
-                        * @param {object} response.invalid_settings
-                        * @returns {void}
-                        */
-                       _handleInvalidSettingsError: function( response ) {
-                               var invalidControls = [], wasFocused = false;
-                               if ( _.isEmpty( response.invalid_settings ) ) {
-                                       return;
-                               }
-
-                               // Find the controls that correspond to each invalid setting.
-                               _.each( response.invalid_settings, function( notifications, settingId ) {
-                                       var setting = api( settingId );
-                                       if ( setting ) {
-                                               _.each( notifications, function( notificationParams, code ) {
-                                                       var notification = new api.Notification( code, notificationParams );
-                                                       setting.notifications.add( code, notification );
-                                               } );
-                                       }
-
-                                       api.control.each( function( control ) {
-                                               _.each( control.settings, function( controlSetting ) {
-                                                       if ( controlSetting.id === settingId ) {
-                                                               invalidControls.push( control );
-                                                       }
-                                               } );
-                                       } );
-                               } );
-
-                               // Focus on the first control that is inside of an expanded section (one that is visible).
-                               _( invalidControls ).find( function( control ) {
-                                       var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
-                                       if ( isExpanded && control.expanded ) {
-                                               isExpanded = control.expanded();
-                                       }
-                                       if ( isExpanded ) {
-                                               control.focus();
-                                               wasFocused = true;
-                                       }
-                                       return wasFocused;
-                               } );
-
-                               // Focus on the first invalid control.
-                               if ( ! wasFocused && invalidControls[0] ) {
-                                       invalidControls[0].focus();
-                               }
-                       },
-
</del><span class="cx" style="display: block; padding: 0 10px">                         save: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                var self = this,
</span><span class="cx" style="display: block; padding: 0 10px">                                        processing = api.state( 'processing' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        submitWhenDoneProcessing,
</span><span class="cx" style="display: block; padding: 0 10px">                                        submit,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        modifiedWhileSaving = {};
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 modifiedWhileSaving = {},
+                                       invalidSettings = [],
+                                       invalidControls;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                body.addClass( 'saving' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3502,6 +3483,27 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                submit = function () {
</span><span class="cx" style="display: block; padding: 0 10px">                                        var request, query;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                       /*
+                                        * Block saving if there are any settings that are marked as
+                                        * invalid from the client (not from the server). Focus on
+                                        * the control.
+                                        */
+                                       api.each( function( setting ) {
+                                               setting.notifications.each( function( notification ) {
+                                                       if ( 'error' === notification.type && ( ! notification.data || ! notification.data.from_server ) ) {
+                                                               invalidSettings.push( setting.id );
+                                                       }
+                                               } );
+                                       } );
+                                       invalidControls = api.findControlsForSettings( invalidSettings );
+                                       if ( ! _.isEmpty( invalidControls ) ) {
+                                               _.values( invalidControls )[0][0].focus();
+                                               body.removeClass( 'saving' );
+                                               api.unbind( 'change', captureSettingModifiedDuringSave );
+                                               return;
+                                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                         query = $.extend( self.query(), {
</span><span class="cx" style="display: block; padding: 0 10px">                                                nonce:  self.nonce.save
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3512,18 +3514,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.trigger( 'save', request );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        /*
-                                        * Remove all setting error notifications prior to save, allowing
-                                        * server to respond with fresh validation error notifications.
-                                        */
-                                       api.each( function( setting ) {
-                                               setting.notifications.each( function( notification ) {
-                                                       if ( 'error' === notification.type ) {
-                                                               setting.notifications.remove( notification.code );
-                                                       }
-                                               } );
-                                       } );
-
</del><span class="cx" style="display: block; padding: 0 10px">                                         request.always( function () {
</span><span class="cx" style="display: block; padding: 0 10px">                                                body.removeClass( 'saving' );
</span><span class="cx" style="display: block; padding: 0 10px">                                                saveBtn.prop( 'disabled', false );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3548,7 +3538,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                        } );
</span><span class="cx" style="display: block; padding: 0 10px">                                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                self._handleInvalidSettingsError( response );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         if ( response.setting_validities ) {
+                                                       api._handleSettingValidities( {
+                                                               settingValidities: response.setting_validities,
+                                                               focusInvalidControl: true
+                                                       } );
+                                               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.trigger( 'error', response );
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3564,6 +3559,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.previewer.send( 'saved', response );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                if ( response.setting_validities ) {
+                                                       api._handleSettingValidities( {
+                                                               settingValidities: response.setting_validities,
+                                                               focusInvalidControl: true
+                                                       } );
+                                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 api.trigger( 'saved', response );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                // Restore the global dirty state if any settings were modified during save.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3670,6 +3672,103 @@
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * Handle setting_validities in an error response for the customize-save request.
+                *
+                * Add notifications to the settings and focus on the first control that has an invalid setting.
+                *
+                * @since 4.6.0
+                * @private
+                *
+                * @param {object}  args
+                * @param {object}  args.settingValidities
+                * @param {boolean} [args.focusInvalidControl=false]
+                * @returns {void}
+                */
+               api._handleSettingValidities = function handleSettingValidities( args ) {
+                       var invalidSettingControls, invalidSettings = [], wasFocused = false;
+
+                       // Find the controls that correspond to each invalid setting.
+                       _.each( args.settingValidities, function( validity, settingId ) {
+                               var setting = api( settingId );
+                               if ( setting ) {
+
+                                       // Add notifications for invalidities.
+                                       if ( _.isObject( validity ) ) {
+                                               _.each( validity, function( params, code ) {
+                                                       var notification = new api.Notification( code, params ), existingNotification, needsReplacement = false;
+
+                                                       // Remove existing notification if already exists for code but differs in parameters.
+                                                       existingNotification = setting.notifications( notification.code );
+                                                       if ( existingNotification ) {
+                                                               needsReplacement = ( notification.type !== existingNotification.type ) || ! _.isEqual( notification.data, existingNotification.data );
+                                                       }
+                                                       if ( needsReplacement ) {
+                                                               setting.notifications.remove( code );
+                                                       }
+
+                                                       if ( ! setting.notifications.has( notification.code ) ) {
+                                                               setting.notifications.add( code, notification );
+                                                       }
+                                                       invalidSettings.push( setting.id );
+                                               } );
+                                       }
+
+                                       // Remove notification errors that are no longer valid.
+                                       setting.notifications.each( function( notification ) {
+                                               if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
+                                                       setting.notifications.remove( notification.code );
+                                               }
+                                       } );
+                               }
+                       } );
+
+                       if ( args.focusInvalidControl ) {
+                               invalidSettingControls = api.findControlsForSettings( invalidSettings );
+
+                               // Focus on the first control that is inside of an expanded section (one that is visible).
+                               _( _.values( invalidSettingControls ) ).find( function( controls ) {
+                                       return _( controls ).find( function( control ) {
+                                               var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
+                                               if ( isExpanded && control.expanded ) {
+                                                       isExpanded = control.expanded();
+                                               }
+                                               if ( isExpanded ) {
+                                                       control.focus();
+                                                       wasFocused = true;
+                                               }
+                                               return wasFocused;
+                                       } );
+                               } );
+
+                               // Focus on the first invalid control.
+                               if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
+                                       _.values( invalidSettingControls )[0][0].focus();
+                               }
+                       }
+               };
+
+               /**
+                * Find all controls associated with the given settings.
+                *
+                * @since 4.6.0
+                * @param {string[]} settingIds Setting IDs.
+                * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
+                */
+               api.findControlsForSettings = function findControlsForSettings( settingIds ) {
+                       var controls = {}, settingControls;
+                       _.each( _.unique( settingIds ), function( settingId ) {
+                               var setting = api( settingId );
+                               if ( setting ) {
+                                       settingControls = setting.findControls();
+                                       if ( settingControls && settingControls.length > 0 ) {
+                                               controls[ settingId ] = settingControls;
+                                       }
+                               }
+                       } );
+                       return controls;
+               };
+
+               /**
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Sort panels, sections, controls by priorities. Hide empty sections and panels.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 4.1.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4040,6 +4139,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="cx" style="display: block; padding: 0 10px">                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Update the setting validities.
+               api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
+                       api._handleSettingValidities( {
+                               settingValidities: settingValidities,
+                               focusInvalidControl: false
+                       } );
+               } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Focus on the control that is associated with the given setting.
</span><span class="cx" style="display: block; padding: 0 10px">                api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        var matchedControl;
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -825,6 +825,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_settings() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
+               $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'theme' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'stylesheet' => $this->get_stylesheet(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -837,6 +840,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'activePanels' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'activeSections' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'activeControls' => array(),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'settingValidities' => $exported_setting_validities,
</ins><span class="cx" style="display: block; padding: 0 10px">                         'nonce' => $this->get_nonces(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'l10n' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -991,12 +995,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.6.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_REST_Request::has_valid_params()
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @see WP_Customize_Setting::validate()
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $setting_values Mapping of setting IDs to values to sanitize and validate.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return array Empty array if all settings were valid. One or more instances of `WP_Error` if any were invalid.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function validate_setting_values( $setting_values ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $validity_errors = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $validities = array();
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $setting_values as $setting_id => $unsanitized_value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = $this->get_setting( $setting_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! $setting || is_null( $unsanitized_value ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1006,11 +1011,46 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( false === $validity || null === $validity ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( is_wp_error( $validity ) ) {
-                               $validity_errors[ $setting_id ] = $validity;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $validities[ $setting_id ] = $validity;
+               }
+               return $validities;
+       }
+
+       /**
+        * Prepare setting validity for exporting to the client (JS).
+        *
+        * Converts `WP_Error` instance into array suitable for passing into the
+        * `wp.customize.Notification` JS model.
+        *
+        * @since 4.6.0
+        * @access public
+        *
+        * @param true|WP_Error $validity Setting validity.
+        * @return true|array If `$validity` was `WP_Error` then array mapping the error
+        *                    codes to their respective `message` and `data` to pass
+        *                    into the `wp.customize.Notification` JS model.
+        */
+       public function prepare_setting_validity_for_js( $validity ) {
+               if ( is_wp_error( $validity ) ) {
+                       $notification = array();
+                       foreach ( $validity->errors as $error_code => $error_messages ) {
+                               $error_data = $validity->get_error_data( $error_code );
+                               if ( is_null( $error_data ) ) {
+                                       $error_data = array();
+                               }
+                               $error_data = array_merge(
+                                       $error_data,
+                                       array( 'from_server' => true )
+                               );
+                               $notification[ $error_code ] = array(
+                                       'message' => join( ' ', $error_messages ),
+                                       'data' => $error_data,
+                               );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        return $notification;
+               } else {
+                       return true;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $validity_errors;
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1041,22 +1081,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_save_validation_before', $this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Validate settings.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $validity_errors = $this->validate_setting_values( $this->unsanitized_post_values() );
-               $invalid_count = count( $validity_errors );
-               if ( $invalid_count > 0 ) {
-                       $settings_errors = array();
-                       foreach ( $validity_errors as $setting_id => $validity_error ) {
-                               $settings_errors[ $setting_id ] = array();
-                               foreach ( $validity_error->errors as $error_code => $error_messages ) {
-                                       $settings_errors[ $setting_id ][ $error_code ] = array(
-                                               'message' => join( ' ', $error_messages ),
-                                               'data' => $validity_error->get_error_data( $error_code ),
-                                       );
-                               }
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
+               $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
+               $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
+               if ( $invalid_setting_count > 0 ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         $response = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'invalid_settings' => $settings_errors,
-                               'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_count ), number_format_i18n( $invalid_count ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'setting_validities' => $exported_setting_validities,
+                               'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1097,6 +1128,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_save_after', $this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $data = array(
+                       'setting_validities' => $exported_setting_validities,
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters response data for a successful customize_save AJAX request.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1108,7 +1143,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 *                                   event on `wp.customize`.
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $response = apply_filters( 'customize_save_response', array(), $this );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $response = apply_filters( 'customize_save_response', $data, $this );
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_send_json_success( $response );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludescustomizeclasswpcustomizeselectiverefreshphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php  2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php    2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -402,6 +402,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $response['errors'] = $this->triggered_errors;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $setting_validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
+               $exported_setting_validities = array_map( array( $this->manager, 'prepare_setting_validity_for_js' ), $setting_validities );
+               $response['setting_validities'] = $exported_setting_validities;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the response from rendering the partials.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/js/customize-preview.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview.js     2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/src/wp-includes/js/customize-preview.js       2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -172,7 +172,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                api.preview.send( 'ready', {
</span><span class="cx" style="display: block; padding: 0 10px">                        activePanels: api.settings.activePanels,
</span><span class="cx" style="display: block; padding: 0 10px">                        activeSections: api.settings.activeSections,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        activeControls: api.settings.activeControls
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 activeControls: api.settings.activeControls,
+                       settingValidities: api.settings.settingValidities
</ins><span class="cx" style="display: block; padding: 0 10px">                 } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Display a loading indicator when preview is reloading, and remove on failure.
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizeselectiverefreshjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/js/customize-selective-refresh.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-selective-refresh.js   2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/src/wp-includes/js/customize-selective-refresh.js     2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -847,6 +847,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /**
+                * Handle setting validities in partial refresh response.
+                *
+                * @param {object} data Response data.
+                * @param {object} data.setting_validities Setting validities.
+                */
+               api.selectiveRefresh.bind( 'render-partials-response', function handleSettingValiditiesResponse( data ) {
+                       if ( data.setting_validities ) {
+                               api.preview.send( 'selective-refresh-setting-validities', data.setting_validities );
+                       }
+               } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 api.preview.bind( 'active', function() {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Make all partials ready.
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/customize/manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/manager.php   2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/tests/phpunit/tests/customize/manager.php     2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -196,7 +196,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::validate_setting_values()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_validate_setting_values() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $default_value = 'foo_default';
</del><span class="cx" style="display: block; padding: 0 10px">                 $setting = $this->manager->add_setting( 'foo', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'validate_callback' => array( $this, 'filter_customize_validate_foo' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -204,7 +203,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $post_value = 'bar';
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->set_post_value( 'foo', $post_value );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEmpty( $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
+               $this->assertCount( 1, $validities );
+               $this->assertEquals( array( 'foo' => true ), $validities );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->set_post_value( 'foo', 'return_wp_error_in_sanitize' );
</span><span class="cx" style="display: block; padding: 0 10px">                $invalid_settings = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -234,6 +235,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test WP_Customize_Manager::prepare_setting_validity_for_js().
+        *
+        * @see WP_Customize_Manager::prepare_setting_validity_for_js()
+        */
+       function test_prepare_setting_validity_for_js() {
+               $this->assertTrue( $this->manager->prepare_setting_validity_for_js( true ) );
+               $error = new WP_Error();
+               $error->add( 'bad_letter', 'Bad letter' );
+               $error->add( 'bad_letter', 'Bad letra' );
+               $error->add( 'bad_number', 'Bad number', array( 'number' => 123 ) );
+               $validity = $this->manager->prepare_setting_validity_for_js( $error );
+               $this->assertInternalType( 'array', $validity );
+               foreach ( $error->errors as $code => $messages ) {
+                       $this->assertArrayHasKey( $code, $validity );
+                       $this->assertInternalType( 'array', $validity[ $code ] );
+                       $this->assertEquals( join( ' ', $messages ), $validity[ $code ]['message'] );
+                       $this->assertArrayHasKey( 'data', $validity[ $code ] );
+                       $this->assertArrayHasKey( 'from_server', $validity[ $code ]['data'] );
+               }
+               $this->assertArrayHasKey( 'number', $validity['bad_number']['data'] );
+               $this->assertEquals( 123, $validity['bad_number']['data']['number'] );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Manager::set_post_value().
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::set_post_value()
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -565,6 +590,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'activePanels', $settings );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'activeSections', $settings );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'activeControls', $settings );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'settingValidities', $settings );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertArrayHasKey( 'nonce', $settings );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( '_dirty', $settings );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizeselectiverefreshajaxphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php    2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php      2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -344,6 +344,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $count_customize_render_partials_after + 1, has_action( 'customize_render_partials_after' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $output = json_decode( ob_get_clean(), true );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( array( get_bloginfo( 'name', 'display' ) ), $output['data']['contents']['test_blogname'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'setting_validities', $output['data'] );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-controls.js       2016-06-14 18:13:03 UTC (rev 37699)
+++ trunk/tests/qunit/wp-admin/js/customize-controls.js 2016-06-14 19:16:54 UTC (rev 37700)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -96,6 +96,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                ok( setting.notifications.extended( wp.customize.Values ) );
</span><span class="cx" style="display: block; padding: 0 10px">                equal( wp.customize.Notification, setting.notifications.prototype.constructor.defaultConstructor );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        test( 'Setting has findControls method', function() {
+               var controls, setting = wp.customize( 'fixture-setting' );
+               equal( 'function', typeof setting.findControls );
+               controls = setting.findControls();
+               equal( 1, controls.length );
+               equal( 'fixture-control', controls[0].id );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px">         test( 'Setting constructor object exists', function( assert ) {
</span><span class="cx" style="display: block; padding: 0 10px">                assert.ok( _.isObject( wp.customize.settingConstructor ) );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -505,4 +512,16 @@
</span><span class="cx" style="display: block; padding: 0 10px">        test( 'Panel instance is not contextuallyActive', function () {
</span><span class="cx" style="display: block; padding: 0 10px">                equal( mockPanel.isContextuallyActive(), false );
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       module( 'Test wp.customize.findControlsForSettings' );
+       test( 'findControlsForSettings(blogname)', function() {
+               var controlsForSettings, settingId = 'fixture-setting', controlId = 'fixture-control';
+               ok( wp.customize.control.has( controlId ) );
+               ok( wp.customize.has( settingId ) );
+               controlsForSettings = wp.customize.findControlsForSettings( [ settingId ] );
+               ok( _.isObject( controlsForSettings ), 'Response is object' );
+               ok( _.isArray( controlsForSettings['fixture-setting'] ), 'Response has a fixture-setting array' );
+               equal( 1, controlsForSettings['fixture-setting'].length );
+               equal( wp.customize.control( controlId ), controlsForSettings['fixture-setting'][0] );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> });
</span></span></pre>
</div>
</div>

</body>
</html>