Commit af587a15 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Support for tests that respond to text input (#7915)

parent c08bac83
...@@ -5,21 +5,7 @@ ...@@ -5,21 +5,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'mock_text_input.dart';
void main() { void main() {
MockTextInput mockTextInput = new MockTextInput()..register();
void enterText(String text) {
mockTextInput.enterText(text);
}
Future<Null> showKeyboard(WidgetTester tester) async {
EditableTextState editable = tester.state(find.byType(EditableText).first);
editable.requestKeyboard();
await tester.pump();
}
testWidgets('onSaved callback is called', (WidgetTester tester) async { testWidgets('onSaved callback is called', (WidgetTester tester) async {
GlobalKey<FormState> formKey = new GlobalKey<FormState>(); GlobalKey<FormState> formKey = new GlobalKey<FormState>();
String fieldValue; String fieldValue;
...@@ -38,12 +24,11 @@ void main() { ...@@ -38,12 +24,11 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
expect(fieldValue, isNull); expect(fieldValue, isNull);
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
formKey.currentState.save(); formKey.currentState.save();
// pump'ing is unnecessary because callback happens regardless of frames // pump'ing is unnecessary because callback happens regardless of frames
...@@ -70,12 +55,11 @@ void main() { ...@@ -70,12 +55,11 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
expect(fieldValue, isNull); expect(fieldValue, isNull);
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
// pump'ing is unnecessary because callback happens regardless of frames // pump'ing is unnecessary because callback happens regardless of frames
expect(fieldValue, equals(testValue)); expect(fieldValue, equals(testValue));
...@@ -107,11 +91,10 @@ void main() { ...@@ -107,11 +91,10 @@ void main() {
// Start off not autovalidating. // Start off not autovalidating.
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
await showKeyboard(tester);
Future<Null> checkErrorText(String testValue) async { Future<Null> checkErrorText(String testValue) async {
formKey.currentState.reset(); formKey.currentState.reset();
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
...@@ -123,7 +106,7 @@ void main() { ...@@ -123,7 +106,7 @@ void main() {
// Try again with autovalidation. Should validate immediately. // Try again with autovalidation. Should validate immediately.
formKey.currentState.reset(); formKey.currentState.reset();
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(true)); await tester.pumpWidget(builder(true));
...@@ -166,10 +149,9 @@ void main() { ...@@ -166,10 +149,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
Future<Null> checkErrorText(String testValue) async { Future<Null> checkErrorText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText).first, testValue);
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
...@@ -200,11 +182,11 @@ void main() { ...@@ -200,11 +182,11 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester); await tester.showKeyboard(find.byType(EditableText));
// initial value should be loaded into keyboard editing state // initial value should be loaded into keyboard editing state
expect(mockTextInput.editingState, isNotNull); expect(tester.testTextInput.editingState, isNotNull);
expect(mockTextInput.editingState['text'], equals(initialValue)); expect(tester.testTextInput.editingState['text'], equals(initialValue));
// initial value should also be visible in the raw input line // initial value should also be visible in the raw input line
EditableTextState editableText = tester.state(find.byType(EditableText)); EditableTextState editableText = tester.state(find.byType(EditableText));
...@@ -212,7 +194,7 @@ void main() { ...@@ -212,7 +194,7 @@ void main() {
// sanity check, make sure we can still edit the text and everything updates // sanity check, make sure we can still edit the text and everything updates
expect(inputKey.currentState.value.text, equals(initialValue)); expect(inputKey.currentState.value.text, equals(initialValue));
enterText('world'); await tester.enterText(find.byType(EditableText), 'world');
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
expect(inputKey.currentState.value.text, equals('world')); expect(inputKey.currentState.value.text, equals('world'));
...@@ -241,12 +223,11 @@ void main() { ...@@ -241,12 +223,11 @@ void main() {
} }
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
await showKeyboard(tester);
expect(fieldValue, isNull); expect(fieldValue, isNull);
expect(formKey.currentState.validate(), isTrue); expect(formKey.currentState.validate(), isTrue);
enterText('Test'); await tester.enterText(find.byType(EditableText), 'Test');
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
......
...@@ -10,8 +10,6 @@ import 'package:flutter/material.dart'; ...@@ -10,8 +10,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'mock_text_input.dart';
class MockClipboard { class MockClipboard {
Object _clipboardData = <String, dynamic>{ Object _clipboardData = <String, dynamic>{
'text': null 'text': null
...@@ -41,7 +39,6 @@ Widget overlay(Widget child) { ...@@ -41,7 +39,6 @@ Widget overlay(Widget child) {
} }
void main() { void main() {
MockTextInput mockTextInput = new MockTextInput()..register();
MockClipboard mockClipboard = new MockClipboard(); MockClipboard mockClipboard = new MockClipboard();
PlatformMessages.setMockJSONMessageHandler('flutter/platform', mockClipboard.handleJSONMessage); PlatformMessages.setMockJSONMessageHandler('flutter/platform', mockClipboard.handleJSONMessage);
...@@ -53,20 +50,6 @@ void main() { ...@@ -53,20 +50,6 @@ void main() {
kThreeLines + kThreeLines +
'Fourth line won\'t display and ends at abcdef ghi. '; 'Fourth line won\'t display and ends at abcdef ghi. ';
void updateEditingState(TextEditingState state) {
mockTextInput.updateEditingState(state);
}
void enterText(String text) {
mockTextInput.enterText(text);
}
Future<Null> showKeyboard(WidgetTester tester) async {
EditableTextState editable = tester.state(find.byType(EditableText).first);
editable.requestKeyboard();
await tester.pump();
}
// Returns the first RenderEditable. // Returns the first RenderEditable.
RenderEditable findRenderEditable(WidgetTester tester) { RenderEditable findRenderEditable(WidgetTester tester) {
RenderObject root = tester.renderObject(find.byType(EditableText)); RenderObject root = tester.renderObject(find.byType(EditableText));
...@@ -101,7 +84,6 @@ void main() { ...@@ -101,7 +84,6 @@ void main() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new Input(
autofocus: true,
value: inputValue, value: inputValue,
key: inputKey, key: inputKey,
hintText: 'Placeholder', hintText: 'Placeholder',
...@@ -119,7 +101,7 @@ void main() { ...@@ -119,7 +101,7 @@ void main() {
Size emptyInputSize = inputBox.size; Size emptyInputSize = inputBox.size;
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
...@@ -152,7 +134,7 @@ void main() { ...@@ -152,7 +134,7 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester); await tester.showKeyboard(find.byType(EditableText));
EditableTextState editableText = tester.state(find.byType(EditableText)); EditableTextState editableText = tester.state(find.byType(EditableText));
...@@ -172,9 +154,10 @@ void main() { ...@@ -172,9 +154,10 @@ void main() {
} }
await checkCursorToggle(); await checkCursorToggle();
await tester.showKeyboard(find.byType(EditableText));
// Try the test again with a nonempty EditableText. // Try the test again with a nonempty EditableText.
updateEditingState(const TextEditingState( tester.testTextInput.updateEditingState(const TextEditingState(
text: 'X', text: 'X',
selectionBase: 1, selectionBase: 1,
selectionExtent: 1, selectionExtent: 1,
...@@ -198,10 +181,10 @@ void main() { ...@@ -198,10 +181,10 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester); await tester.showKeyboard(find.byType(EditableText));
const String testValue = 'ABC'; const String testValue = 'ABC';
updateEditingState(const TextEditingState( tester.testTextInput.updateEditingState(const TextEditingState(
text: testValue, text: testValue,
selectionBase: testValue.length, selectionBase: testValue.length,
selectionExtent: testValue.length, selectionExtent: testValue.length,
...@@ -235,10 +218,9 @@ void main() { ...@@ -235,10 +218,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
String testValue = 'abc def ghi'; String testValue = 'abc def ghi';
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
expect(inputValue.text, testValue); expect(inputValue.text, testValue);
...@@ -283,10 +265,9 @@ void main() { ...@@ -283,10 +265,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
String testValue = 'abc def ghi'; String testValue = 'abc def ghi';
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -359,10 +340,9 @@ void main() { ...@@ -359,10 +340,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
String testValue = 'abc def ghi'; String testValue = 'abc def ghi';
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -425,10 +405,9 @@ void main() { ...@@ -425,10 +405,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
String testValue = 'abc def ghi'; String testValue = 'abc def ghi';
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -476,20 +455,19 @@ void main() { ...@@ -476,20 +455,19 @@ void main() {
} }
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
await showKeyboard(tester);
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
RenderBox inputBox = findInputBox(); RenderBox inputBox = findInputBox();
Size emptyInputSize = inputBox.size; Size emptyInputSize = inputBox.size;
enterText('No wrapping here.'); await tester.enterText(find.byType(EditableText), 'No wrapping here.');
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize)); expect(inputBox.size, equals(emptyInputSize));
enterText(kThreeLines); await tester.enterText(find.byType(EditableText), kThreeLines);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
...@@ -498,14 +476,14 @@ void main() { ...@@ -498,14 +476,14 @@ void main() {
Size threeLineInputSize = inputBox.size; Size threeLineInputSize = inputBox.size;
// An extra line won't increase the size because we max at 3. // An extra line won't increase the size because we max at 3.
enterText(kFourLines); await tester.enterText(find.byType(EditableText), kFourLines);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize); expect(inputBox.size, threeLineInputSize);
// But now it will. // But now it will.
enterText(kFourLines); await tester.enterText(find.byType(EditableText), kFourLines);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(4)); await tester.pumpWidget(builder(4));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
...@@ -539,11 +517,10 @@ void main() { ...@@ -539,11 +517,10 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
String testValue = kThreeLines; String testValue = kThreeLines;
String cutValue = 'First line of stuff keeps going until abcdef ghijk. '; String cutValue = 'First line of stuff keeps going until abcdef ghijk. ';
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -632,9 +609,8 @@ void main() { ...@@ -632,9 +609,8 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
enterText(kFourLines); await tester.enterText(find.byType(EditableText), kFourLines);
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -718,10 +694,9 @@ void main() { ...@@ -718,10 +694,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
...@@ -751,10 +726,9 @@ void main() { ...@@ -751,10 +726,9 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await showKeyboard(tester);
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
enterText(testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
...@@ -828,5 +802,4 @@ void main() { ...@@ -828,5 +802,4 @@ void main() {
expect(iconRight, equals(tester.getTopLeft(find.text('label')).x)); expect(iconRight, equals(tester.getTopLeft(find.text('label')).x));
expect(iconRight, equals(tester.getTopLeft(find.byType(InputField)).x)); expect(iconRight, equals(tester.getTopLeft(find.byType(InputField)).x));
}); });
} }
...@@ -15,5 +15,6 @@ export 'src/matchers.dart'; ...@@ -15,5 +15,6 @@ export 'src/matchers.dart';
export 'src/test_async_utils.dart'; export 'src/test_async_utils.dart';
export 'src/stack_manipulation.dart'; export 'src/stack_manipulation.dart';
export 'src/test_pointer.dart'; export 'src/test_pointer.dart';
export 'src/test_text_input.dart';
export 'src/test_vsync.dart'; export 'src/test_vsync.dart';
export 'src/widget_tester.dart'; export 'src/widget_tester.dart';
...@@ -20,6 +20,7 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -20,6 +20,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'stack_manipulation.dart'; import 'stack_manipulation.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_text_input.dart';
/// Phases that can be reached by [WidgetTester.pumpWidget] and /// Phases that can be reached by [WidgetTester.pumpWidget] and
/// [TestWidgetsFlutterBinding.pump]. /// [TestWidgetsFlutterBinding.pump].
...@@ -125,6 +126,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -125,6 +126,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
); );
}); });
}; };
_testTextInput = new TestTextInput()..register();
super.initInstances(); super.initInstances();
} }
...@@ -188,6 +190,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -188,6 +190,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
super.dispatchEvent(event, result); super.dispatchEvent(event, result);
} }
/// A stub for the system's onscreen keyboard. Callers must set the
/// [focusedEditable] before using this value.
TestTextInput get testTextInput => _testTextInput;
TestTextInput _testTextInput;
/// The current client of the onscreen keyboard. Callers must pump
/// an additional frame after setting this property to complete the
/// the focus change.
EditableTextState get focusedEditable => _focusedEditable;
EditableTextState _focusedEditable;
set focusedEditable (EditableTextState editable) {
_focusedEditable = editable..requestKeyboard();
}
/// Returns the exception most recently caught by the Flutter framework. /// Returns the exception most recently caught by the Flutter framework.
/// ///
/// Call this if you expect an exception during a test. If an exception is /// Call this if you expect an exception during a test. If an exception is
......
...@@ -6,17 +6,27 @@ import 'dart:async'; ...@@ -6,17 +6,27 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'widget_tester.dart';
const String _kTextInputClientChannel = 'flutter/textinputclient'; const String _kTextInputClientChannel = 'flutter/textinputclient';
class MockTextInput { /// A testing stub for the system's onscreen keyboard.
///
/// Typical app tests will not need to use this class directly.
///
/// See also:
///
/// * [WidgetTester.enterText], which uses this class to simulate keyboard input.
/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
/// popup keyboard and initializing its text.
class TestTextInput {
void register() { void register() {
PlatformMessages.setMockJSONMessageHandler('flutter/textinput', handleJSONMessage); PlatformMessages.setMockJSONMessageHandler('flutter/textinput', handleJSONMessage);
} }
int client = 0; int _client = 0;
Map<String, dynamic> editingState; Map<String, dynamic> editingState;
Future<dynamic> handleJSONMessage(dynamic message) async { Future<dynamic> handleJSONMessage(dynamic message) async {
...@@ -24,7 +34,7 @@ class MockTextInput { ...@@ -24,7 +34,7 @@ class MockTextInput {
final List<dynamic> args= message['args']; final List<dynamic> args= message['args'];
switch (method) { switch (method) {
case 'TextInput.setClient': case 'TextInput.setClient':
client = args[0]; _client = args[0];
break; break;
case 'TextInput.setEditingState': case 'TextInput.setEditingState':
editingState = args[0]; editingState = args[0];
...@@ -33,10 +43,10 @@ class MockTextInput { ...@@ -33,10 +43,10 @@ class MockTextInput {
} }
void updateEditingState(TextEditingState state) { void updateEditingState(TextEditingState state) {
expect(client, isNonZero); expect(_client, isNonZero);
String message = JSON.encode(<String, dynamic>{ String message = JSON.encode(<String, dynamic>{
'method': 'TextInputClient.updateEditingState', 'method': 'TextInputClient.updateEditingState',
'args': <dynamic>[client, state.toJSON()], 'args': <dynamic>[_client, state.toJSON()],
}); });
Uint8List encoded = UTF8.encoder.convert(message); Uint8List encoded = UTF8.encoder.convert(message);
PlatformMessages.handlePlatformMessage( PlatformMessages.handlePlatformMessage(
......
...@@ -15,6 +15,7 @@ import 'binding.dart'; ...@@ -15,6 +15,7 @@ import 'binding.dart';
import 'controller.dart'; import 'controller.dart';
import 'finders.dart'; import 'finders.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_text_input.dart';
export 'package:test/test.dart' hide expect; export 'package:test/test.dart' hide expect;
...@@ -399,6 +400,36 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -399,6 +400,36 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
void _endOfTestVerifications() { void _endOfTestVerifications() {
verifyTickersWereDisposed('at the end of the test'); verifyTickersWereDisposed('at the end of the test');
} }
/// Returns the TestTextInput singleton.
///
/// Typical app tests will not need to use this value. To add text to widgets
/// like [Input] or [TextField], call [enterText].
TestTextInput get testTextInput => binding.testTextInput;
/// Give the EditableText widget specified by [finder] the focus, as if the
/// onscreen keyboard had appeared.
///
/// Tests that just need to add text to widgets like [Input] or [TextField]
/// only need to call [enterText].
Future<Null> showKeyboard(Finder finder) async {
// TODO(hansmuller): Once find.descendant (#7789) lands replace the following
// RHS with state(find.descendant(finder), find.byType(EditableText)).
final EditableTextState editable = state(finder);
if (editable != binding.focusedEditable) {
binding.focusedEditable = editable;
await pump();
}
return null;
}
/// Give the EditableText widget specified by [finder] the focus and
/// enter [text] as if it been provided by the onscreen keyboard.
Future<Null> enterText(Finder finder, String text) async {
await showKeyboard(finder);
testTextInput.enterText(text);
return null;
}
} }
typedef void _TickerDisposeCallback(_TestTicker ticker); typedef void _TickerDisposeCallback(_TestTicker ticker);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment