diff --git src/wp-admin/js/widgets/text-widgets.js src/wp-admin/js/widgets/text-widgets.js
index 282090d585..692a0642c9 100644
--- src/wp-admin/js/widgets/text-widgets.js
+++ src/wp-admin/js/widgets/text-widgets.js
@@ -368,7 +368,7 @@ wp.textWidgets = ( function( $ ) {
 		}
 
 		// Bypass using TinyMCE when widget is in legacy mode.
-		if ( widgetForm.find( '.legacy' ).length > 0 ) {
+		if ( ! widgetForm.find( '.visual' ).val() ) {
 			return;
 		}
 
@@ -429,7 +429,7 @@ wp.textWidgets = ( function( $ ) {
 		}
 
 		// Bypass using TinyMCE when widget is in legacy mode.
-		if ( widgetForm.find( '.legacy' ).length > 0 ) {
+		if ( ! widgetForm.find( '.visual' ).val() ) {
 			return;
 		}
 
diff --git src/wp-includes/widgets/class-wp-widget-custom-html.php src/wp-includes/widgets/class-wp-widget-custom-html.php
index 7fcce78f19..82f1ff944c 100644
--- src/wp-includes/widgets/class-wp-widget-custom-html.php
+++ src/wp-includes/widgets/class-wp-widget-custom-html.php
@@ -61,8 +61,14 @@ class WP_Widget_Custom_HTML extends WP_Widget {
 		/** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
 		$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
 
+		// Prepare instance data that looks like a normal Text widget.
+		$simulated_text_widget_instance = $instance;
+		$simulated_text_widget_instance['text'] = $simulated_text_widget_instance['content'];
+		unset( $simulated_text_widget_instance['content'] );
+		$simulated_text_widget_instance['filter'] = false; // Because wpautop is not applied.
+
 		/** This filter is documented in wp-includes/widgets/class-wp-widget-text.php */
-		$content = apply_filters( 'widget_text', $instance['content'], $instance, $this );
+		$content = apply_filters( 'widget_text', $instance['content'], $simulated_text_widget_instance, $this );
 
 		/**
 		 * Filters the content of the Custom HTML widget.
diff --git src/wp-includes/widgets/class-wp-widget-text.php src/wp-includes/widgets/class-wp-widget-text.php
index 2297d2bee2..0335a3e489 100644
--- src/wp-includes/widgets/class-wp-widget-text.php
+++ src/wp-includes/widgets/class-wp-widget-text.php
@@ -79,12 +79,12 @@ class WP_Widget_Text extends WP_Widget {
 	 */
 	public function is_legacy_instance( $instance ) {
 
-		// If the widget has been updated while in legacy mode, it stays in legacy mode.
-		if ( ! empty( $instance['legacy'] ) ) {
-			return true;
+		// Legacy mode when not in visual mode.
+		if ( isset( $instance['visual'] ) ) {
+			return ! $instance['visual'];
 		}
 
-		// If the widget has been added/updated in 4.8 then filter prop is 'content' and it is no longer legacy.
+		// Or, the widget has been added/updated in 4.8.0 then filter prop is 'content' and it is no longer legacy.
 		if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) {
 			return false;
 		}
@@ -193,7 +193,16 @@ class WP_Widget_Text extends WP_Widget {
 		$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
 
 		$text = ! empty( $instance['text'] ) ? $instance['text'] : '';
-		$is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] );
+		$is_visual_text_widget = ( ! empty( $instance['visual'] ) && ! empty( $instance['filter'] ) );
+
+		// In 4.8.0 only, visual Text widgets get filter=content, without visual prop; upgrade instance props just-in-time.
+		if ( ! $is_visual_text_widget ) {
+			$is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] );
+		}
+		if ( $is_visual_text_widget ) {
+			$instance['filter'] = true;
+			$instance['visual'] = true;
+		}
 
 		/*
 		 * Just-in-time temporarily upgrade Visual Text widget shortcode handling
@@ -221,25 +230,23 @@ class WP_Widget_Text extends WP_Widget {
 		 */
 		$text = apply_filters( 'widget_text', $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         $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', $text, $instance, $this );
-
-			} elseif ( $instance['filter'] ) {
-				$text = wpautop( $text ); // Back-compat for instances prior to 4.8.
-			}
+		if ( $is_visual_text_widget ) {
+
+			/**
+			 * 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         $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', $text, $instance, $this );
+
+		} elseif ( ! empty( $instance['filter'] ) ) {
+			$text = wpautop( $text ); // Back-compat for instances prior to 4.8.
 		}
 
 		// Undo temporary upgrade of the plugin-supplied shortcode handling.
@@ -271,7 +278,15 @@ class WP_Widget_Text extends WP_Widget {
 	 * @return array Settings to save or bool false to cancel saving.
 	 */
 	public function update( $new_instance, $old_instance ) {
+		$new_instance = wp_parse_args( $new_instance, array(
+			'title' => '',
+			'text' => '',
+			'filter' => false, // For back-compat.
+			'visual' => null, // Must be explicitly defined.
+		) );
+
 		$instance = $old_instance;
+
 		$instance['title'] = sanitize_text_field( $new_instance['title'] );
 		if ( current_user_can( 'unfiltered_html' ) ) {
 			$instance['text'] = $new_instance['text'];
@@ -279,20 +294,23 @@ class WP_Widget_Text extends WP_Widget {
 			$instance['text'] = wp_kses_post( $new_instance['text'] );
 		}
 
-		/*
-		 * If the Text widget is in legacy mode, then a hidden input will indicate this
-		 * and the new content value for the filter prop will by bypassed. Otherwise,
-		 * 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.
-		 */
-		if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) {
-			$instance['filter'] = ! empty( $new_instance['filter'] );
-			$instance['legacy'] = true;
-		} else {
-			$instance['filter'] = 'content';
-			unset( $instance['legacy'] );
+		$instance['filter'] = ! empty( $new_instance['filter'] );
+
+		// Upgrade 4.8.0 format.
+		if ( isset( $old_instance['filter'] ) && 'content' === $old_instance['filter'] ) {
+			$instance['visual'] = true;
+		}
+		if ( 'content' === $new_instance['filter'] ) {
+			$instance['visual'] = true;
+		}
+
+		if ( isset( $new_instance['visual'] ) ) {
+			$instance['visual'] = ! empty( $new_instance['visual'] );
+		}
+
+		// Filter is always true in visual mode.
+		if ( ! empty( $instance['visual'] ) ) {
+			$instance['filter'] = true;
 		}
 
 		return $instance;
@@ -333,8 +351,10 @@ class WP_Widget_Text extends WP_Widget {
 		<?php if ( ! $this->is_legacy_instance( $instance ) ) : ?>
 			<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'] ); ?>">
+			<input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" class="filter" type="hidden" value="1">
+			<input id="<?php echo $this->get_field_id( 'visual' ); ?>" name="<?php echo $this->get_field_name( 'visual' ); ?>" class="visual" type="hidden" value="1">
 		<?php else : ?>
-			<input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true">
+			<input id="<?php echo $this->get_field_id( 'visual' ); ?>" name="<?php echo $this->get_field_name( 'visual' ); ?>" class="visual" type="hidden" value="">
 			<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( $instance['title'] ); ?>"/>
diff --git tests/phpunit/tests/widgets/custom-html-widget.php tests/phpunit/tests/widgets/custom-html-widget.php
index dcd03d2007..3d39b673cc 100644
--- tests/phpunit/tests/widgets/custom-html-widget.php
+++ tests/phpunit/tests/widgets/custom-html-widget.php
@@ -59,6 +59,13 @@ class Test_WP_Widget_Custom_HTML extends WP_UnitTestCase {
 			'content' => $content,
 		);
 
+		// Convert Custom HTML widget instance into Text widget instance data.
+		$text_widget_instance = array_merge( $instance, array(
+			'filter' => false,
+			'text' => $instance['content'],
+		) );
+		unset( $text_widget_instance['content'] );
+
 		update_option( 'use_balanceTags', 0 );
 		add_filter( 'widget_custom_html_content', array( $this, 'filter_widget_custom_html_content' ), 5, 3 );
 		add_filter( 'widget_text', array( $this, 'filter_widget_text' ), 10, 3 );
@@ -75,11 +82,11 @@ class Test_WP_Widget_Custom_HTML extends WP_UnitTestCase {
 		$this->assertNotContains( '<p>', $output );
 		$this->assertNotContains( '<br>', $output );
 		$this->assertNotContains( '</u>', $output );
-		$this->assertEquals( $instance, $this->widget_text_args[1] );
+		$this->assertEquals( $text_widget_instance, $this->widget_text_args[1] );
 		$this->assertEquals( $instance, $this->widget_custom_html_content_args[1] );
 		$this->assertSame( $widget, $this->widget_text_args[2] );
 		$this->assertSame( $widget, $this->widget_custom_html_content_args[2] );
-		remove_filter( 'widget_custom_html_content', array( $this, 'filter_widget_custom_html_content' ), 5, 3 );
+		remove_filter( 'widget_custom_html_content', array( $this, 'filter_widget_custom_html_content' ), 5 );
 		remove_filter( 'widget_text', array( $this, 'filter_widget_text' ), 10 );
 
 		update_option( 'use_balanceTags', 1 );
diff --git tests/phpunit/tests/widgets/text-widget.php tests/phpunit/tests/widgets/text-widget.php
index 764b4eaeac..c65e2e0f5c 100644
--- tests/phpunit/tests/widgets/text-widget.php
+++ tests/phpunit/tests/widgets/text-widget.php
@@ -82,17 +82,18 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 			'before_widget' => '<section>',
 			'after_widget'  => "</section>\n",
 		);
-		$instance = array(
-			'title'  => 'Foo',
-			'text'   => $text,
-			'filter' => false,
-		);
 
 		add_filter( 'widget_text_content', array( $this, 'filter_widget_text_content' ), 5, 3 );
 		add_filter( 'widget_text', array( $this, 'filter_widget_text' ), 5, 3 );
 
-		// Test with filter=false.
+		// Test with filter=false, implicit legacy mode.
+		$this->widget_text_content_args = null;
 		ob_start();
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => false,
+		);
 		$widget->widget( $args, $instance );
 		$output = ob_get_clean();
 		$this->assertNotContains( '<p>', $output );
@@ -102,8 +103,13 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		$this->assertContains( '[filter:widget_text]', $output );
 		$this->assertNotContains( '[filter:widget_text_content]', $output );
 
-		// Test with filter=true.
-		$instance['filter'] = true;
+		// Test with filter=true, implicit legacy mode.
+		$this->widget_text_content_args = null;
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => true,
+		);
 		ob_start();
 		$widget->widget( $args, $instance );
 		$output = ob_get_clean();
@@ -117,22 +123,97 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		$this->assertContains( '[filter:widget_text]', $output );
 		$this->assertNotContains( '[filter:widget_text_content]', $output );
 
-		// Test with filter=content, the upgraded widget.
-		$instance['filter'] = 'content';
+		// Test with filter=content, the upgraded widget, in 4.8.0 only.
+		$this->widget_text_content_args = null;
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => 'content',
+		);
+		$expected_instance = array_merge( $instance, array(
+			'filter' => true,
+			'visual' => true,
+		) );
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertContains( '<p>', $output );
+		$this->assertContains( '<br />', $output );
+		$this->assertCount( 3, $this->widget_text_args );
+		$this->assertEquals( $expected_instance['text'], $this->widget_text_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_args[1] );
+		$this->assertEquals( $widget, $this->widget_text_args[2] );
+		$this->assertCount( 3, $this->widget_text_content_args );
+		$this->assertEquals( $expected_instance['text'] . '[filter:widget_text]', $this->widget_text_content_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_content_args[1] );
+		$this->assertEquals( $widget, $this->widget_text_content_args[2] );
+		$this->assertContains( wpautop( $expected_instance['text'] . '[filter:widget_text][filter:widget_text_content]' ), $output );
+
+		// Test with filter=true&visual=true, the upgraded widget, in 4.8.1 and above.
+		$this->widget_text_content_args = null;
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => true,
+			'visual' => true,
+		);
+		$expected_instance = $instance;
 		ob_start();
 		$widget->widget( $args, $instance );
 		$output = ob_get_clean();
 		$this->assertContains( '<p>', $output );
 		$this->assertContains( '<br />', $output );
 		$this->assertCount( 3, $this->widget_text_args );
-		$this->assertEquals( $instance['text'], $this->widget_text_args[0] );
-		$this->assertEquals( $instance, $this->widget_text_args[1] );
+		$this->assertEquals( $expected_instance['text'], $this->widget_text_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_args[1] );
 		$this->assertEquals( $widget, $this->widget_text_args[2] );
 		$this->assertCount( 3, $this->widget_text_content_args );
-		$this->assertEquals( $instance['text'] . '[filter:widget_text]', $this->widget_text_content_args[0] );
-		$this->assertEquals( $instance, $this->widget_text_content_args[1] );
+		$this->assertEquals( $expected_instance['text'] . '[filter:widget_text]', $this->widget_text_content_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_content_args[1] );
 		$this->assertEquals( $widget, $this->widget_text_content_args[2] );
-		$this->assertContains( wpautop( $instance['text'] . '[filter:widget_text][filter:widget_text_content]' ), $output );
+		$this->assertContains( wpautop( $expected_instance['text'] . '[filter:widget_text][filter:widget_text_content]' ), $output );
+
+		// Test with filter=true&visual=true, the upgraded widget, in 4.8.1 and above.
+		$this->widget_text_content_args = null;
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => true,
+			'visual' => false,
+		);
+		$expected_instance = $instance;
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertContains( '<p>', $output );
+		$this->assertContains( '<br />', $output );
+		$this->assertCount( 3, $this->widget_text_args );
+		$this->assertEquals( $expected_instance['text'], $this->widget_text_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_args[1] );
+		$this->assertEquals( $widget, $this->widget_text_args[2] );
+		$this->assertNull( $this->widget_text_content_args );
+		$this->assertContains( wpautop( $expected_instance['text'] . '[filter:widget_text]' ), $output );
+
+		// Test with filter=false&visual=false, the upgraded widget, in 4.8.1 and above.
+		$this->widget_text_content_args = null;
+		$instance = array(
+			'title'  => 'Foo',
+			'text'   => $text,
+			'filter' => false,
+			'visual' => false,
+		);
+		$expected_instance = $instance;
+		ob_start();
+		$widget->widget( $args, $instance );
+		$output = ob_get_clean();
+		$this->assertNotContains( '<p>', $output );
+		$this->assertNotContains( '<br />', $output );
+		$this->assertCount( 3, $this->widget_text_args );
+		$this->assertEquals( $expected_instance['text'], $this->widget_text_args[0] );
+		$this->assertEquals( $expected_instance, $this->widget_text_args[1] );
+		$this->assertEquals( $widget, $this->widget_text_args[2] );
+		$this->assertNull( $this->widget_text_content_args );
+		$this->assertContains( $expected_instance['text'] . '[filter:widget_text]', $output );
 	}
 
 	/**
@@ -250,14 +331,19 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		);
 
 		$instance = array_merge( $base_instance, array(
-			'legacy' => true,
+			'visual' => false,
 		) );
-		$this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when legacy prop is present.' );
+		$this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when visual=false prop is present.' );
+
+		$instance = array_merge( $base_instance, array(
+			'visual' => true,
+		) );
+		$this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when visual=true prop is present.' );
 
 		$instance = array_merge( $base_instance, array(
 			'filter' => 'content',
 		) );
-		$this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content.' );
+		$this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content (in WP 4.8.0 only).' );
 
 		$instance = array_merge( $base_instance, array(
 			'text' => '',
@@ -364,13 +450,14 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 			'title' => 'Title',
 			'text' => 'Text',
 			'filter' => false,
-			'legacy' => true,
+			'visual' => false,
 		);
 		$this->assertTrue( $widget->is_legacy_instance( $instance ) );
 		ob_start();
 		$widget->form( $instance );
 		$form = ob_get_clean();
-		$this->assertContains( 'class="legacy"', $form );
+		$this->assertContains( 'class="visual" type="hidden" value=""', $form );
+		$this->assertNotContains( 'class="visual" type="hidden" value="1"', $form );
 
 		$instance = array(
 			'title' => 'Title',
@@ -381,7 +468,33 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		ob_start();
 		$widget->form( $instance );
 		$form = ob_get_clean();
-		$this->assertNotContains( 'class="legacy"', $form );
+		$this->assertContains( 'class="visual" type="hidden" value="1"', $form );
+		$this->assertNotContains( 'class="visual" type="hidden" value=""', $form );
+
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+			'filter' => true,
+		);
+		$this->assertFalse( $widget->is_legacy_instance( $instance ) );
+		ob_start();
+		$widget->form( $instance );
+		$form = ob_get_clean();
+		$this->assertContains( 'class="visual" type="hidden" value="1"', $form );
+		$this->assertNotContains( 'class="visual" type="hidden" value=""', $form );
+
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+			'filter' => true,
+			'visual' => true,
+		);
+		$this->assertFalse( $widget->is_legacy_instance( $instance ) );
+		ob_start();
+		$widget->form( $instance );
+		$form = ob_get_clean();
+		$this->assertContains( 'class="visual" type="hidden" value="1"', $form );
+		$this->assertNotContains( 'class="visual" type="hidden" value=""', $form );
 	}
 
 	/**
@@ -394,30 +507,30 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		$instance = array(
 			'title' => "The\nTitle",
 			'text'  => "The\n\nText",
-			'filter' => 'content',
+			'filter' => true,
+			'visual' => true,
 		);
 
 		wp_set_current_user( $this->factory()->user->create( array(
 			'role' => 'administrator',
 		) ) );
 
-		// Should return valid instance in legacy mode since filter=false and there are line breaks.
 		$expected = array(
 			'title'  => sanitize_text_field( $instance['title'] ),
 			'text'   => $instance['text'],
-			'filter' => 'content',
+			'filter' => true,
+			'visual' => true,
 		);
 		$result = $widget->update( $instance, array() );
 		$this->assertEquals( $expected, $result );
 		$this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
 
-		// Make sure KSES is applying as expected.
 		add_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ), 10, 2 );
 		$this->assertTrue( current_user_can( 'unfiltered_html' ) );
 		$instance['text'] = '<script>alert( "Howdy!" );</script>';
 		$expected['text'] = $instance['text'];
 		$result = $widget->update( $instance, array() );
-		$this->assertEquals( $expected, $result );
+		$this->assertEquals( $expected, $result, 'KSES should apply as expected.' );
 		remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
 
 		add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
@@ -425,7 +538,7 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 		$instance['text'] = '<script>alert( "Howdy!" );</script>';
 		$expected['text'] = wp_kses_post( $instance['text'] );
 		$result = $widget->update( $instance, array() );
-		$this->assertEquals( $expected, $result );
+		$this->assertEquals( $expected, $result, 'KSES should not apply since user can unfiltered_html.' );
 		remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
 	}
 
@@ -437,48 +550,150 @@ class Test_WP_Widget_Text extends WP_UnitTestCase {
 	function test_update_legacy() {
 		$widget = new WP_Widget_Text();
 
-		// Updating a widget with explicit filter=true persists with legacy mode.
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+			'filter' => false,
+		);
+		$result = $widget->update( $instance, array() );
+		$this->assertEquals( $instance, $result, 'Updating a widget without visual prop and explicit filter=false leaves visual prop absent' );
+
+		// --
 		$instance = array(
 			'title' => 'Legacy',
 			'text' => 'Text',
 			'filter' => true,
 		);
 		$result = $widget->update( $instance, array() );
+		$this->assertEquals( $instance, $result, 'Updating a widget without visual prop and explicit filter=true leaves legacy prop absent.' );
+
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+			'visual' => true,
+		);
+		$old_instance = array_merge( $instance, array(
+			'filter' => false,
+		) );
 		$expected = array_merge( $instance, array(
-			'legacy' => true,
 			'filter' => true,
 		) );
-		$this->assertEquals( $expected, $result );
+		$result = $widget->update( $instance, $old_instance );
+		$this->assertEquals( $expected, $result, 'Updating a pre-existing widget with visual mode forces filter to be true.' );
 
-		// Updating a widget with explicit filter=false persists with legacy mode.
-		$instance['filter'] = false;
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+			'filter' => true,
+		);
+		$old_instance = array_merge( $instance, array(
+			'visual' => true,
+		) );
+		$result = $widget->update( $instance, $old_instance );
+		$expected = array_merge( $instance, array(
+			'visual' => true,
+		) );
+		$this->assertEquals( $expected, $result, 'Updating a pre-existing visual widget retains visual mode when updated.' );
+
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+		);
+		$old_instance = array_merge( $instance, array(
+			'visual' => true,
+		) );
+		$result = $widget->update( $instance, $old_instance );
+		$expected = array_merge( $instance, array(
+			'visual' => true,
+			'filter' => true,
+		) );
+		$this->assertEquals( $expected, $result, 'Updating a pre-existing visual widget retains visual=true and supplies missing filter=true.' );
+
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+			'visual' => true,
+		);
+		$expected = array_merge( $instance, array(
+			'filter' => true,
+		) );
+		$result = $widget->update( $instance, array() );
+		$this->assertEquals( $expected, $result, 'Updating a widget with explicit visual=true and absent filter prop causes filter to be set to true.' );
+
+		// --
+		$instance = array(
+			'title' => 'Legacy',
+			'text' => 'Text',
+			'visual' => false,
+		);
 		$result = $widget->update( $instance, array() );
 		$expected = array_merge( $instance, array(
-			'legacy' => true,
 			'filter' => false,
 		) );
-		$this->assertEquals( $expected, $result );
+		$this->assertEquals( $expected, $result, 'Updating a widget in legacy mode results in filter=false as if checkbox not checked.' );
+
+		// --
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+			'filter' => false,
+		);
+		$old_instance = array_merge( $instance, array(
+			'visual' => false,
+			'filter' => true,
+		) );
+		$result = $widget->update( $instance, $old_instance );
+		$expected = array_merge( $instance, array(
+			'visual' => false,
+			'filter' => false,
+		) );
+		$this->assertEquals( $expected, $result, 'Updating a widget that previously had legacy form results in filter allowed to be false.' );
 
-		// Updating a widget in legacy form results in filter=false when checkbox not checked.
-		$instance['filter'] = true;
+		// --
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+			'filter' => 'content',
+		);
 		$result = $widget->update( $instance, array() );
 		$expected = array_merge( $instance, array(
-			'legacy' => true,
 			'filter' => true,
+			'visual' => true,
 		) );
-		$this->assertEquals( $expected, $result );
+		$this->assertEquals( $expected, $result, 'Updating a widget that had \'content\' as its filter value persists non-legacy mode. This only existed in WP 4.8.0.' );
 
-		// Updating a widget that previously had legacy form results in filter persisting.
-		unset( $instance['legacy'] );
-		$instance['filter'] = true;
-		$result = $widget->update( $instance, array(
-			'legacy' => true,
+		// --
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+		);
+		$old_instance = array_merge( $instance, array(
+			'filter' => 'content',
 		) );
+		$result = $widget->update( $instance, $old_instance );
 		$expected = array_merge( $instance, array(
-			'legacy' => true,
+			'visual' => true,
 			'filter' => true,
 		) );
-		$this->assertEquals( $expected, $result );
+		$this->assertEquals( $expected, $result, 'Updating a pre-existing widget with the filter=content prop in WP 4.8.0 upgrades to filter=true&visual=true.' );
+
+		// --
+		$instance = array(
+			'title' => 'Title',
+			'text' => 'Text',
+			'filter' => 'content',
+		);
+		$result = $widget->update( $instance, array() );
+		$expected = array_merge( $instance, array(
+			'filter' => true,
+			'visual' => true,
+		) );
+		$this->assertEquals( $expected, $result, 'Updating a widget with filter=content (from WP 4.8.0) upgrades to filter=true&visual=true.' );
 	}
 
 	/**
