Index: Gruntfile.js
===================================================================
--- Gruntfile.js	(revision 40625)
+++ Gruntfile.js	(working copy)
@@ -456,7 +456,7 @@
 				dest: BUILD_DIR,
 				ext: '.min.js',
 				src: [
-					'wp-admin/js/*.js',
+					'wp-admin/js/**/*.js',
 					'wp-includes/js/*.js',
 					'wp-includes/js/mediaelement/wp-mediaelement.js',
 					'wp-includes/js/mediaelement/wp-playlist.js',
Index: src/wp-admin/css/customize-widgets.css
===================================================================
--- src/wp-admin/css/customize-widgets.css	(revision 40625)
+++ src/wp-admin/css/customize-widgets.css	(working copy)
@@ -213,6 +213,21 @@
 	display: block;
 }
 
+/* Text Widget */
+.wp-customizer div.mce-inline-toolbar-grp,
+.wp-customizer div.mce-tooltip {
+	z-index: 500100 !important;
+}
+.wp-customizer .ui-autocomplete.wplink-autocomplete {
+	z-index: 500110; /* originally 100110, but z-index of .wp-full-overlay is 500000 */
+}
+.wp-customizer #wp-link-backdrop {
+	z-index: 500100; /* originally 100100, but z-index of .wp-full-overlay is 500000 */
+}
+.wp-customizer #wp-link-wrap {
+	z-index: 500105; /* originally 100105, but z-index of .wp-full-overlay is 500000 */
+}
+
 /**
  * Styles for new widget addition panel
  */
Index: src/wp-admin/js/widgets/text-widgets.js
===================================================================
--- src/wp-admin/js/widgets/text-widgets.js	(nonexistent)
+++ src/wp-admin/js/widgets/text-widgets.js	(working copy)
@@ -0,0 +1,313 @@
+/* global tinymce, switchEditors */
+/* eslint consistent-this: [ "error", "control" ] */
+wp.textWidgets = ( function( $ ) {
+	'use strict';
+
+	var component = {};
+
+	/**
+	 * Text widget control.
+	 *
+	 * @class TextWidgetControl
+	 * @constructor
+	 * @abstract
+	 */
+	component.TextWidgetControl = Backbone.View.extend({
+
+		/**
+		 * View events.
+		 *
+		 * @type {Object}
+		 */
+		events: {},
+
+		/**
+		 * Initialize.
+		 *
+		 * @param {Object}         options - Options.
+		 * @param {Backbone.Model} options.model - Model.
+		 * @param {jQuery}         options.el - Control container element.
+		 * @returns {void}
+		 */
+		initialize: function initialize( options ) {
+			var control = this;
+
+			if ( ! options.el ) {
+				throw new Error( 'Missing options.el' );
+			}
+
+			Backbone.View.prototype.initialize.call( control, options );
+
+			/*
+			 * Create a container element for the widget control fields.
+			 * This is inserted into the DOM immediately before the the .widget-content
+			 * element because the contents of this element are essentially "managed"
+			 * by PHP, where each widget update cause the entire element to be emptied
+			 * and replaced with the rendered output of WP_Widget::form() which is
+			 * sent back in Ajax request made to save/update the widget instance.
+			 * To prevent a "flash of replaced DOM elements and re-initialized JS
+			 * components", the JS template is rendered outside of the normal form
+			 * container.
+			 */
+			control.fieldContainer = $( '<div class="text-widget-fields"></div>' );
+			control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) );
+			control.widgetContentContainer = control.$el.find( '.widget-content:first' );
+			control.widgetContentContainer.before( control.fieldContainer );
+
+			control.fields = {
+				title: control.fieldContainer.find( '.title' ),
+				text: control.fieldContainer.find( '.text' )
+			};
+
+			// Sync input fields to hidden sync fields which actually get sent to the server.
+			_.each( control.fields, function( fieldInput, fieldName ) {
+				fieldInput.on( 'input change', function updateSyncField() {
+					var syncInput = control.widgetContentContainer.find( 'input[type=hidden].' + fieldName );
+					if ( syncInput.val() !== $( this ).val() ) {
+						syncInput.val( $( this ).val() );
+						syncInput.trigger( 'change' );
+					}
+				});
+
+				// Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
+				fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ).val() );
+			});
+		},
+
+		/**
+		 * Update input fields from the sync fields.
+		 *
+		 * This function is called at the widget-updated and widget-synced events.
+		 * A field will only be updated if it is not currently focused, to avoid
+		 * overwriting content that the user is entering.
+		 *
+		 * @returns {void}
+		 */
+		updateFields: function updateFields() {
+			var control = this, syncInput;
+
+			if ( ! control.fields.title.is( document.activeElement ) ) {
+				syncInput = control.widgetContentContainer.find( 'input[type=hidden].title' );
+				control.fields.title.val( syncInput.val() );
+			}
+
+			syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' );
+			if ( control.fields.text.is( ':visible' ) ) {
+				if ( ! control.fields.text.is( document.activeElement ) ) {
+					control.fields.text.val( syncInput.val() );
+				}
+			} else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) {
+				control.editor.setContent( wp.editor.autop( syncInput.val() ) );
+			}
+		},
+
+		/**
+		 * Initialize editor.
+		 *
+		 * @returns {void}
+		 */
+		initializeEditor: function initializeEditor() {
+			var control = this, changeDebounceDelay = 1000, iframeKeepAliveInterval = 1000, id, textarea, restoreTextMode = false;
+			textarea = control.fields.text;
+			id = textarea.attr( 'id' );
+
+			/**
+			 * Build (or re-build) the visual editor.
+			 *
+			 * @returns {void}
+			 */
+			function buildEditor() {
+				var editor, triggerChangeIfDirty, onInit;
+
+				// Destroy any existing editor so that it can be re-initialized after a widget-updated event.
+				if ( tinymce.get( id ) )    {
+					restoreTextMode = tinymce.get( id ).isHidden();
+					wp.editor.remove( id );
+				}
+
+				wp.editor.initialize( id, {
+					tinymce: {
+						wpautop: true
+					},
+					quicktags: true
+				} );
+
+				editor = window.tinymce.get( id );
+				if ( ! editor ) {
+					throw new Error( 'Failed to initialize editor' );
+				}
+				onInit = function() {
+					watchForDestroyedBody( control.$el.find( 'iframe' )[0] );
+
+					// If a prior mce instance was replaced, and it was in text mode, toggle to text mode.
+					if ( restoreTextMode ) {
+						switchEditors.go( id, 'toggle' );
+					}
+				};
+
+				if ( editor.initialized ) {
+					onInit();
+				} else {
+					editor.on( 'init', onInit );
+				}
+
+				control.editorFocused = false;
+				triggerChangeIfDirty = function() {
+					if ( editor.isDirty() ) {
+						editor.save();
+						textarea.trigger( 'change' );
+					}
+				};
+				editor.on( 'focus', function() {
+					control.editorFocused = true;
+				} );
+				editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) );
+				editor.on( 'blur', function() {
+					control.editorFocused = false;
+					triggerChangeIfDirty();
+				} );
+
+				control.editor = editor;
+			}
+
+			/**
+			 * Watch an iframe for the destruction of its TinyMCE contenteditable contents.
+			 *
+			 * @todo There may be a better way to listen for an iframe being destroyed.
+			 * @param {HTMLIFrameElement} iframe - TinyMCE iframe.
+			 * @returns {void}
+			 */
+			function watchForDestroyedBody( iframe ) {
+				var timeoutId = setInterval( function() {
+					if ( ! iframe.contentWindow || iframe.contentWindow.document.body.id ) {
+						return;
+					}
+					clearInterval( timeoutId );
+					buildEditor();
+				}, iframeKeepAliveInterval );
+			}
+
+			buildEditor();
+		}
+	});
+
+	/**
+	 * Mapping of widget ID to instances of TextWidgetControl subclasses.
+	 *
+	 * @type {Object.<string, wp.textWidgets.TextWidgetControl>}
+	 */
+	component.widgetControls = {};
+
+	/**
+	 * Handle widget being added or initialized for the first time at the widget-added event.
+	 *
+	 * @param {jQuery.Event} event - Event.
+	 * @param {jQuery}       widgetContainer - Widget container element.
+	 * @returns {void}
+	 */
+	component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
+		var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, widgetInside, renderWhenAnimationDone;
+		widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
+
+		idBase = widgetForm.find( '> .id_base' ).val();
+		if ( 'text' !== idBase ) {
+			return;
+		}
+
+		// Prevent initializing already-added widgets.
+		widgetId = widgetForm.find( '> .widget-id' ).val();
+		if ( component.widgetControls[ widgetId ] ) {
+			return;
+		}
+
+		widgetControl = new component.TextWidgetControl({
+			el: widgetContainer
+		});
+
+		component.widgetControls[ widgetId ] = widgetControl;
+
+		/*
+		 * Render the widget once the widget parent's container finishes animating,
+		 * as the widget-added event fires with a slideDown of the container.
+		 * This ensures that the textarea is visible and an iframe can be embedded
+		 * with TinyMCE being able to set contenteditable on it.
+		 */
+		widgetInside = widgetContainer.parent();
+		renderWhenAnimationDone = function() {
+			if ( widgetInside.is( ':animated' ) ) {
+				setTimeout( renderWhenAnimationDone, animatedCheckDelay );
+			} else {
+				widgetControl.initializeEditor();
+			}
+		};
+		renderWhenAnimationDone();
+	};
+
+	/**
+	 * Sync widget instance data sanitized from server back onto widget model.
+	 *
+	 * This gets called via the 'widget-updated' event when saving a widget from
+	 * the widgets admin screen and also via the 'widget-synced' event when making
+	 * a change to a widget in the customizer.
+	 *
+	 * @param {jQuery.Event} event - Event.
+	 * @param {jQuery}       widgetContainer - Widget container element.
+	 * @returns {void}
+	 */
+	component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
+		var widgetForm, widgetId, widgetControl, idBase;
+		widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+
+		idBase = widgetForm.find( '> .id_base' ).val();
+		if ( 'text' !== idBase ) {
+			return;
+		}
+
+		widgetId = widgetForm.find( '> .widget-id' ).val();
+		widgetControl = component.widgetControls[ widgetId ];
+		if ( ! widgetControl ) {
+			return;
+		}
+
+		widgetControl.updateFields();
+	};
+
+	/**
+	 * Initialize functionality.
+	 *
+	 * This function exists to prevent the JS file from having to boot itself.
+	 * When WordPress enqueues this script, it should have an inline script
+	 * attached which calls wp.textWidgets.init().
+	 *
+	 * @returns {void}
+	 */
+	component.init = function init() {
+		var $document = $( document );
+		$document.on( 'widget-added', component.handleWidgetAdded );
+		$document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
+
+		/*
+		 * Manually trigger widget-added events for media widgets on the admin
+		 * screen once they are expanded. The widget-added event is not triggered
+		 * for each pre-existing widget on the widgets admin screen like it is
+		 * on the customizer. Likewise, the customizer only triggers widget-added
+		 * when the widget is expanded to just-in-time construct the widget form
+		 * when it is actually going to be displayed. So the following implements
+		 * the same for the widgets admin screen, to invoke the widget-added
+		 * handler when a pre-existing media widget is expanded.
+		 */
+		$( function initializeExistingWidgetContainers() {
+			var widgetContainers;
+			if ( 'widgets' !== window.pagenow ) {
+				return;
+			}
+			widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
+			widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
+				var widgetContainer = $( this );
+				component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
+			});
+		});
+	};
+
+	return component;
+})( jQuery );
Index: src/wp-content/themes/twentyten/style.css
===================================================================
--- src/wp-content/themes/twentyten/style.css	(revision 40625)
+++ src/wp-content/themes/twentyten/style.css	(working copy)
@@ -841,6 +841,9 @@
 	padding: 4px;
 	text-align: center;
 }
+.widget-container .wp-caption {
+	max-width: 100% !important;
+}
 .wp-caption img {
 	margin: 5px 5px 0;
 	max-width: 622px; /* caption width - 10px */
Index: src/wp-includes/default-filters.php
===================================================================
--- src/wp-includes/default-filters.php	(revision 40625)
+++ src/wp-includes/default-filters.php	(working copy)
@@ -164,7 +164,11 @@
 
 add_filter( 'wp_sprintf', 'wp_sprintf_l', 10, 2 );
 
-add_filter( 'widget_text', 'balanceTags' );
+add_filter( 'widget_text',         'balanceTags'          );
+add_filter( 'widget_text_content', 'capital_P_dangit', 11 );
+add_filter( 'widget_text_content', 'wptexturize'          );
+add_filter( 'widget_text_content', 'convert_smilies',  20 );
+add_filter( 'widget_text_content', 'wpautop'              );
 
 add_filter( 'date_i18n', 'wp_maybe_decline_date' );
 
Index: src/wp-includes/script-loader.php
===================================================================
--- src/wp-includes/script-loader.php	(revision 40625)
+++ src/wp-includes/script-loader.php	(working copy)
@@ -602,6 +602,8 @@
 		$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
 
 		$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
+		$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
+		$scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
 
 		$scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
 
Index: src/wp-includes/widgets/class-wp-widget-text.php
===================================================================
--- src/wp-includes/widgets/class-wp-widget-text.php	(revision 40625)
+++ src/wp-includes/widgets/class-wp-widget-text.php	(working copy)
@@ -28,11 +28,31 @@
 			'description' => __( 'Arbitrary text or HTML.' ),
 			'customize_selective_refresh' => true,
 		);
-		$control_ops = array( 'width' => 400, 'height' => 350 );
+		$control_ops = array(
+			'width' => 400,
+			'height' => 350,
+		);
 		parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
 	}
 
 	/**
+	 * Add hooks for enqueueing assets when registering all widget instances of this widget class.
+	 *
+	 * @since 4.8.0
+	 * @access public
+	 */
+	public function _register() {
+
+		// Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts().
+		add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) );
+
+		// Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts().
+		add_action( 'admin_footer-widgets.php', array( $this, 'render_control_template_scripts' ) );
+
+		parent::_register();
+	}
+
+	/**
 	 * Outputs the content for the current Text widget instance.
 	 *
 	 * @since 2.8.0
@@ -61,11 +81,34 @@
 		 */
 		$text = apply_filters( 'widget_text', $widget_text, $instance, $this );
 
+		if ( isset( $instance['filter'] ) ) {
+			if ( 'content' === $instance['filter'] ) {
+
+				/**
+				 * Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor.
+				 *
+				 * By default a subset of the_content filters are applied, including wpautop and wptexturize.
+				 *
+				 * @since 4.8.0
+				 *
+				 * @param string         $widget_text The widget content.
+				 * @param array          $instance    Array of settings for the current widget.
+				 * @param WP_Widget_Text $this        Current Text widget instance.
+				 */
+				$text = apply_filters( 'widget_text_content', $widget_text, $instance, $this );
+
+			} elseif ( $instance['filter'] ) {
+				$text = wpautop( $text ); // Back-compat for instances prior to 4.8.
+			}
+		}
+
 		echo $args['before_widget'];
 		if ( ! empty( $title ) ) {
 			echo $args['before_title'] . $title . $args['after_title'];
-		} ?>
-			<div class="textwidget"><?php echo !empty( $instance['filter'] ) ? wpautop( $text ) : $text; ?></div>
+		}
+
+		?>
+			<div class="textwidget"><?php echo $text; ?></div>
 		<?php
 		echo $args['after_widget'];
 	}
@@ -89,30 +132,73 @@
 		} else {
 			$instance['text'] = wp_kses_post( $new_instance['text'] );
 		}
-		$instance['filter'] = ! empty( $new_instance['filter'] );
+
+		/*
+		 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
+		 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
+		 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
+		 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
+		 */
+		$instance['filter'] = 'content';
+
 		return $instance;
 	}
 
 	/**
+	 * Loads the required scripts and styles for the widget control.
+	 *
+	 * @since 4.8.0
+	 * @access public
+	 */
+	public function enqueue_admin_scripts() {
+		wp_enqueue_editor();
+		wp_enqueue_script( 'text-widgets' );
+	}
+
+	/**
 	 * Outputs the Text widget settings form.
 	 *
 	 * @since 2.8.0
+	 * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
 	 * @access public
+	 * @see WP_Widget_Visual_Text::render_control_template_scripts()
 	 *
 	 * @param array $instance Current settings.
+	 * @return void
 	 */
 	public function form( $instance ) {
-		$instance = wp_parse_args( (array) $instance, array( 'title' => '', 'text' => '' ) );
-		$filter = isset( $instance['filter'] ) ? $instance['filter'] : 0;
-		$title = sanitize_text_field( $instance['title'] );
+		$instance = wp_parse_args(
+			(array) $instance,
+			array(
+				'title' => '',
+				'text' => '',
+			)
+		);
 		?>
-		<p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?></label>
-		<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>" /></p>
-
-		<p><label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
-		<textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id('text'); ?>" name="<?php echo $this->get_field_name('text'); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea></p>
+		<input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
+		<input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
+		<?php
+	}
 
-		<p><input id="<?php echo $this->get_field_id('filter'); ?>" name="<?php echo $this->get_field_name('filter'); ?>" type="checkbox"<?php checked( $filter ); ?> />&nbsp;<label for="<?php echo $this->get_field_id('filter'); ?>"><?php _e('Automatically add paragraphs'); ?></label></p>
+	/**
+	 * Render form template scripts.
+	 *
+	 * @since 4.8.0
+	 * @access public
+	 */
+	public function render_control_template_scripts() {
+		?>
+		<script type="text/html" id="tmpl-widget-text-control-fields">
+			<# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
+			<p>
+				<label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
+				<input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
+			</p>
+			<p>
+				<label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
+				<textarea id="{{ elementIdPrefix }}text" class="widefat text" style="height: 200px" rows="16" cols="20"></textarea>
+			</p>
+		</script>
 		<?php
 	}
 }
Index: tests/phpunit/tests/widgets/test-class-wp-widget-text.php
===================================================================
--- tests/phpunit/tests/widgets/test-class-wp-widget-text.php	(nonexistent)
+++ tests/phpunit/tests/widgets/test-class-wp-widget-text.php	(working copy)
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Unit tests covering WP_Widget_Text functionality.
+ *
+ * @package    WordPress
+ * @subpackage widgets
+ */
+
+/**
+ * Test wp-includes/widgets/class-wp-widget-text.php
+ *
+ * @group widgets
+ */
+class Test_WP_Widget_Text extends WP_UnitTestCase {
+
+	/**
+	 * Test enqueue_admin_scripts method.
+	 *
+	 * @covers WP_Widget_Text::_register
+	 */
+	function test__register() {
+		set_current_screen( 'widgets.php' );
+		$widget = new WP_Widget_Text();
+		$widget->_register();
+
+		$this->assertEquals( 10, has_action( 'admin_print_scripts-widgets.php', array( $widget, 'enqueue_admin_scripts' ) ) );
+		$this->assertEquals( 10, has_action( 'admin_footer-widgets.php', array( $widget, 'render_control_template_scripts' ) ) );
+	}
+
+	/**
+	 * Test widget method.
+	 *
+	 * @covers WP_Widget_Text::widget
+	 */
+	function test_widget() {
+		$widget = new WP_Widget_Text();
+		$text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Praesent ut turpis consequat lorem volutpat bibendum vitae vitae ante.";
+
+		$args = array(
+			'before_title'  => '<h2>',
+			'after_title'   => "</h2>\n",
+			'before_widget' => '<section>',
+			'after_widget'  => "</section>\n",
+		);
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => false,
+		);
+
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertNotContains( '<p>', $output );
+		$this->assertNotContains( '<br />', $output );
+
+		$instance['filter'] = true;
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertContains( '<p>', $output );
+		$this->assertContains( '<br />', $output );
+
+		$instance['filter'] = 'content';
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertContains( '<p>', $output );
+		$this->assertContains( '<br />', $output );
+	}
+
+	/**
+	 * Test update method.
+	 *
+	 * @covers WP_Widget_Text::update
+	 */
+	function test_update() {
+		$widget = new WP_Widget_Text();
+		$instance = array(
+			'text'  => '',
+			'title' => '',
+		);
+
+		wp_set_current_user( $this->factory()->user->create( array(
+			'role' => 'author',
+		) ) );
+
+		// Should return valid instance.
+		$expected = array(
+			'text'   => '',
+			'title'  => '',
+			'filter' => 'content',
+		);
+		$result = $widget->update( $expected, $instance );
+		$this->assertSame( $result, $expected );
+
+		// Back-compat with pre-4.8.
+		$this->assertTrue( ! empty( $expected['filter'] ) );
+
+		wp_get_current_user()->add_cap( 'unfiltered_html' );
+		$expected['text'] = '<script>alert( "Howdy!" );</script>';
+		$result = $widget->update( $expected, $instance );
+		$this->assertSame( $result, $expected );
+	}
+
+	/**
+	 * Test enqueue_admin_scripts method.
+	 *
+	 * @covers WP_Widget_Text::enqueue_admin_scripts
+	 */
+	function test_enqueue_admin_scripts() {
+		set_current_screen( 'widgets.php' );
+		$widget = new WP_Widget_Text();
+		$widget->enqueue_admin_scripts();
+
+		$this->assertTrue( wp_script_is( 'text-widgets' ) );
+	}
+
+	/**
+	 * Test render_control_template_scripts method.
+	 *
+	 * @covers WP_Widget_Text::render_control_template_scripts
+	 */
+	function test_render_control_template_scripts() {
+		$widget = new WP_Widget_Text();
+
+		ob_start();
+		$widget->render_control_template_scripts();
+		$output = ob_get_clean();
+
+		$this->assertContains( '<script type="text/html" id="tmpl-widget-text-control-fields">', $output );
+	}
+}
