diff --git a/dev/bots/test.dart b/dev/bots/test.dart index f29c253a862..30316830519 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -92,7 +92,6 @@ const List kWebTestFileKnownFailures = [ 'test/widgets/selectable_text_test.dart', 'test/widgets/color_filter_test.dart', 'test/widgets/editable_text_cursor_test.dart', - 'test/widgets/editable_text_test.dart', 'test/material/animated_icons_private_test.dart', 'test/material/data_table_test.dart', 'test/cupertino/nav_bar_transition_test.dart', diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 745870124f5..f97cad87432 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1627,6 +1627,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (!_shouldCreateInputConnection) { return; } + if (widget.readOnly) { + // In the read-only case, we only care about selection changes, and reject + // everything else. + value = _value.copyWith(selection: value.selection); + } _receivedRemoteTextEditingValue = value; if (value.text != _value.text) { hideToolbar(); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index a6e7a8e01a0..63a2b7fe017 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -300,7 +300,12 @@ void main() { controller.text = 'test'; await tester.idle(); expect(tester.testTextInput.editingState['text'], equals('test')); - expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.name')); + expect( + tester.testTextInput.setClientArgs['inputType']['name'], + // On web, we don't infer the keyboard type as "name". We only infer + // on iOS and macOS. + kIsWeb ? equals('TextInputType.address') : equals('TextInputType.name'), + ); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets('infer keyboard types from autofillHints: non-ios', @@ -1106,9 +1111,10 @@ void main() { cause: SelectionChangedCause.tap, ); await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pumpAndSettle(); - expect(find.text('Paste'), findsOneWidget); + expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); // Hide the menu again. state.hideToolbar(); @@ -1118,9 +1124,10 @@ void main() { // Can show the menu with text and a selection. controller.text = 'blah'; await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pumpAndSettle(); - expect(find.text('Paste'), findsOneWidget); + expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); }); testWidgets('can show the toolbar after clearing all text', (WidgetTester tester) async { @@ -1158,9 +1165,10 @@ void main() { await tester.pump(); // Should be able to show the toolbar. - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pumpAndSettle(); - expect(find.text('Paste'), findsOneWidget); + expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); }); testWidgets('can dynamically disable options in toolbar', (WidgetTester tester) async { @@ -1190,10 +1198,11 @@ void main() { cause: SelectionChangedCause.tap, ); await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pump(); - expect(find.text('Select all'), findsOneWidget); - expect(find.text('Copy'), findsOneWidget); + expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget); + expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Cut'), findsNothing); }); @@ -1220,7 +1229,8 @@ void main() { tester.state(find.byType(EditableText)); await tester.tap(find.byType(EditableText)); await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pump(); expect(find.text('Select All'), findsNothing); expect(find.text('Copy'), findsNothing); @@ -1256,10 +1266,11 @@ void main() { cause: SelectionChangedCause.tap, ); await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pump(); expect(find.text('Select all'), findsNothing); - expect(find.text('Copy'), findsOneWidget); + expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Cut'), findsNothing); }); @@ -1294,10 +1305,11 @@ void main() { cause: SelectionChangedCause.tap, ); await tester.pump(); - expect(state.showToolbar(), true); + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); await tester.pump(); - expect(find.text('Select all'), findsOneWidget); - expect(find.text('Copy'), findsOneWidget); + expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget); + expect(find.text('Copy'), kIsWeb ? findsNothing : findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Cut'), findsNothing); }); @@ -1308,6 +1320,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: EditableText( + readOnly: true, controller: controller, backgroundCursorColor: Colors.grey, focusNode: focusNode, @@ -1317,21 +1330,25 @@ void main() { ), ); - final EditableTextState state = - tester.state(find.byType(EditableText)); + // Interact with the field to establish the input connection. + final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); + await tester.tapAt(topLeft + const Offset(0.0, 5.0)); + await tester.pump(); - // Select the first word "Lorem". - state.renderEditable.selectWordsInRange( - from: Offset.zero, - cause: SelectionChangedCause.tap, - ); + controller.selection = const TextSelection(baseOffset: 0, extentOffset: 5); + await tester.pump(); if (kIsWeb) { // On the web, a regular connection to the platform should've been made // with the `readOnly` flag set to true. expect(tester.testTextInput.hasAnyClients, isTrue); expect(tester.testTextInput.setClientArgs['readOnly'], isTrue); - expect(tester.testTextInput.editingState['text'], equals('Lorem')); + expect( + tester.testTextInput.editingState['text'], + 'Lorem ipsum dolor sit amet', + ); + expect(tester.testTextInput.editingState['selectionBase'], 0); + expect(tester.testTextInput.editingState['selectionExtent'], 5); } else { // On non-web platforms, a read-only field doesn't need a connection with // the platform. @@ -1345,6 +1362,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: EditableText( + readOnly: true, controller: controller, backgroundCursorColor: Colors.grey, focusNode: focusNode, @@ -1354,22 +1372,29 @@ void main() { ), ); - final EditableTextState state = - tester.state(find.byType(EditableText)); - - // Select something. - state.renderEditable.selectWordsInRange( - from: Offset.zero, - cause: SelectionChangedCause.tap, - ); + // Interact with the field to establish the input connection. + final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); + await tester.tapAt(topLeft + const Offset(0.0, 5.0)); + await tester.pump(); expect(tester.testTextInput.hasAnyClients, kIsWeb ? isTrue : isFalse); if (kIsWeb) { // On the web, the input connection exists, but text updates should be // ignored. - tester.testTextInput.enterText('Foo bar'); - // No change. - expect(controller.text, 'Lorem ipsum dolor sit amet'); + tester.testTextInput.updateEditingValue(const TextEditingValue( + text: 'Foo bar', + selection: TextSelection(baseOffset: 0, extentOffset: 3), + composing: TextRange(start: 3, end: 4), + )); + // Only selection should change. + expect( + controller.value, + const TextEditingValue( + text: 'Lorem ipsum dolor sit amet', + selection: TextSelection(baseOffset: 0, extentOffset: 3), + composing: TextRange.empty, + ), + ); } }); @@ -1405,7 +1430,10 @@ void main() { await tester.pump(); expect(changedValue, clipboardContent); - }); + + // On web, we don't show the Flutter toolbar and instead rely on the browser + // toolbar. Until we change that, this test should remain skipped. + }, skip: kIsWeb); // The variants to test in the focus handling test. final ValueVariant focusVariants = ValueVariant< @@ -2271,7 +2299,8 @@ void main() { expect(controller.selection.extentOffset, 9); semantics.dispose(); - }); + // https://github.com/flutter/flutter/issues/64507 + }, skip: kIsWeb); testWidgets('can extend selection with a11y means - character', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -2472,7 +2501,8 @@ void main() { expect(controller.selection.extentOffset, 9); semantics.dispose(); - }); + // https://github.com/flutter/flutter/issues/64507 + }, skip: kIsWeb); testWidgets('password fields have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -3466,7 +3496,10 @@ void main() { // at all. Again, both handles should be invisible. scrollable.controller.jumpTo(0); await verifyVisibility(HandlePositionInViewport.rightEdge, false, HandlePositionInViewport.rightEdge, false); - }); + + // On web, we don't show the Flutter toolbar and instead rely on the browser + // toolbar. Until we change that, this test should remain skipped. + }, skip: kIsWeb); testWidgets('text selection handle visibility RTL', (WidgetTester tester) async { // Text with two separate words to select. @@ -3525,7 +3558,10 @@ void main() { expect(state.selectionOverlay.handlesAreVisible, isTrue); expect(controller.selection.base.offset, 0); expect(controller.selection.extent.offset, 5); - }); + + // On web, we don't show the Flutter toolbar and instead rely on the browser + // toolbar. Until we change that, this test should remain skipped. + }, skip: kIsWeb); const String testText = 'Now is the time for\n' 'all good people\n' @@ -4121,19 +4157,23 @@ void main() { testWidgets('keyboard text selection works as expected on linux', (WidgetTester tester) async { await testTextEditing(tester, platform: 'linux'); - }); + // On web, using keyboard for selection is handled by the browser. + }, skip: kIsWeb); testWidgets('keyboard text selection works as expected on android', (WidgetTester tester) async { await testTextEditing(tester, platform: 'android'); - }); + // On web, using keyboard for selection is handled by the browser. + }, skip: kIsWeb); testWidgets('keyboard text selection works as expected on fuchsia', (WidgetTester tester) async { await testTextEditing(tester, platform: 'fuchsia'); - }); + // On web, using keyboard for selection is handled by the browser. + }, skip: kIsWeb); testWidgets('keyboard text selection works as expected on macos', (WidgetTester tester) async { await testTextEditing(tester, platform: 'macos'); - }); + // On web, using keyboard for selection is handled by the browser. + }, skip: kIsWeb); // Regression test for https://github.com/flutter/flutter/issues/31287 testWidgets('text selection handle visibility', (WidgetTester tester) async { @@ -4301,7 +4341,10 @@ void main() { // at all. Again, both handles should be invisible. scrollable.controller.jumpTo(0); await verifyVisibility(HandlePositionInViewport.rightEdge, false, HandlePositionInViewport.rightEdge, false); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + + // On web, we don't show the Flutter toolbar and instead rely on the browser + // toolbar. Until we change that, this test should remain skipped. + }, skip: kIsWeb, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets("scrolling doesn't bounce", (WidgetTester tester) async { // 3 lines of text, where the last line overflows and requires scrolling.