diff --git a/packages/flutter/lib/src/services/binary_messenger.dart b/packages/flutter/lib/src/services/binary_messenger.dart index 9f9ce15c23..4854c784d5 100644 --- a/packages/flutter/lib/src/services/binary_messenger.dart +++ b/packages/flutter/lib/src/services/binary_messenger.dart @@ -41,20 +41,35 @@ abstract class BinaryMessenger { /// argument. /// /// The handler's return value, if non-null, is sent as a response, unencoded. - void setMessageHandler(String channel, Future handler(ByteData message)); + void setMessageHandler(String channel, MessageHandler handler); + + /// Returns true if the `handler` argument matches the `handler` previously + /// passed to [setMessageHandler]. + /// + /// This method is useful for tests or test harnesses that want to assert the + /// handler for the vspecified channel has not been altered by a previous test. + bool checkMessageHandler(String channel, MessageHandler handler); /// Set a mock callback for intercepting messages from the [send] method on /// this class, on the given channel, without decoding them. /// /// The given callback will replace the currently registered mock callback for /// that channel, if any. To remove the mock handler, pass null as the - /// [handler] argument. + /// `handler` argument. /// /// The handler's return value, if non-null, is used as a response, unencoded. /// /// This is intended for testing. Messages intercepted in this manner are not /// sent to platform plugins. - void setMockMessageHandler(String channel, Future handler(ByteData message)); + void setMockMessageHandler(String channel, MessageHandler handler); + + /// Returns true if the `handler` argument matches the `handler` previously + /// passed to [setMockMessageHandler]. + /// + /// This method is useful for tests or test harnesses that want to assert the + /// mock handler for the specified channel has not been altered by a previous + /// test. + bool checkMockMessageHandler(String channel, MessageHandler handler); } /// The default instance of [BinaryMessenger]. diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index d0acf572e2..a92d83a6ef 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -293,6 +293,9 @@ class _DefaultBinaryMessenger extends BinaryMessenger { }); } + @override + bool checkMessageHandler(String channel, MessageHandler handler) => _handlers[channel] == handler; + @override void setMockMessageHandler(String channel, MessageHandler handler) { if (handler == null) @@ -300,4 +303,7 @@ class _DefaultBinaryMessenger extends BinaryMessenger { else _mockHandlers[channel] = handler; } + + @override + bool checkMockMessageHandler(String channel, MessageHandler handler) => _mockHandlers[channel] == handler; } diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index 47e914199a..0b16c8374a 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -98,6 +98,9 @@ class BasicMessageChannel { } } +Expando _methodChannelHandlers = Expando(); +Expando _methodChannelMockHandlers = Expando(); + /// A named channel for communicating with platform plugins using asynchronous /// method calls. /// @@ -372,12 +375,22 @@ class MethodChannel { /// similarly to what happens if no method call handler has been set. /// Any other exception results in an error envelope being sent. void setMethodCallHandler(Future handler(MethodCall call)) { + _methodChannelHandlers[this] = handler; binaryMessenger.setMessageHandler( name, - handler == null ? null : (ByteData message) => _handleAsMethodCall(message, handler), + handler == null + ? null + : (ByteData message) => _handleAsMethodCall(message, handler), ); } + /// Returns true if the `handler` argument matches the `handler` previously + /// passed to [setMethodCallHandler]. + /// + /// This method is useful for tests or test harnesses that want to assert the + /// handler for the specified channel has not been altered by a previous test. + bool checkMethodCallHandler(Future handler(MethodCall call)) => _methodChannelHandlers[this] == handler; + /// Sets a mock callback for intercepting method invocations on this channel. /// /// The given callback will replace the currently registered mock callback for @@ -397,12 +410,20 @@ class MethodChannel { /// [MethodCodec.encodeSuccessEnvelope], to act as if platform plugin had /// returned that value. void setMockMethodCallHandler(Future handler(MethodCall call)) { + _methodChannelMockHandlers[this] = handler; binaryMessenger.setMockMessageHandler( name, handler == null ? null : (ByteData message) => _handleAsMethodCall(message, handler), ); } + /// Returns true if the `handler` argument matches the `handler` previously + /// passed to [setMockMethodCallHandler]. + /// + /// This method is useful for tests or test harnesses that want to assert the + /// handler for the specified channel has not been altered by a previous test. + bool checkMockMethodCallHandler(Future handler(MethodCall call)) => _methodChannelMockHandlers[this] == handler; + Future _handleAsMethodCall(ByteData message, Future handler(MethodCall call)) async { final MethodCall call = codec.decodeMethodCall(message); try { diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart index e170ab05a2..c7f85bc31a 100644 --- a/packages/flutter/test/services/autofill_test.dart +++ b/packages/flutter/test/services/autofill_test.dart @@ -211,9 +211,15 @@ class FakeTextChannel implements MethodChannel { incoming = handler; } + @override + bool checkMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + @override void setMockMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + @override + bool checkMockMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + void validateOutgoingMethodCalls(List calls) { expect(outgoingCalls.length, calls.length); bool hasError = false; diff --git a/packages/flutter/test/services/default_binary_messenger_test.dart b/packages/flutter/test/services/default_binary_messenger_test.dart index f6df456dbc..74a16e08f3 100644 --- a/packages/flutter/test/services/default_binary_messenger_test.dart +++ b/packages/flutter/test/services/default_binary_messenger_test.dart @@ -34,4 +34,26 @@ void main() { }); expect(count, equals(1)); }); + + test('can check the handler', () { + Future handler(ByteData call) => Future.value(null); + final BinaryMessenger messenger = ServicesBinding.instance.defaultBinaryMessenger; + + expect(messenger.checkMessageHandler('test_channel', null), true); + expect(messenger.checkMessageHandler('test_channel', handler), false); + messenger.setMessageHandler('test_channel', handler); + expect(messenger.checkMessageHandler('test_channel', handler), true); + messenger.setMessageHandler('test_channel', null); + }); + + test('can check the mock handler', () { + Future handler(ByteData call) => Future.value(null); + final BinaryMessenger messenger = ServicesBinding.instance.defaultBinaryMessenger; + + expect(messenger.checkMockMessageHandler('test_channel', null), true); + expect(messenger.checkMockMessageHandler('test_channel', handler), false); + messenger.setMockMessageHandler('test_channel', handler); + expect(messenger.checkMockMessageHandler('test_channel', handler), true); + messenger.setMockMessageHandler('test_channel', null); + }); } diff --git a/packages/flutter/test/services/platform_channel_test.dart b/packages/flutter/test/services/platform_channel_test.dart index 9859a520a5..e3e01c67c5 100644 --- a/packages/flutter/test/services/platform_channel_test.dart +++ b/packages/flutter/test/services/platform_channel_test.dart @@ -58,6 +58,7 @@ void main() { final String result = await channel.invokeMethod('sayHello', 'hello'); expect(result, equals('hello world')); }); + test('can invoke list method and get result', () async { ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler( 'ch7', @@ -89,7 +90,6 @@ void main() { expect(await channel.invokeListMethod('sayHello', 'hello'), null); }); - test('can invoke map method and get result', () async { ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler( 'ch7', @@ -143,6 +143,7 @@ void main() { fail('PlatformException expected'); } }); + test('can invoke unimplemented method', () async { ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler( 'ch7', @@ -158,6 +159,7 @@ void main() { fail('MissingPluginException expected'); } }); + test('can invoke unimplemented method (optional)', () async { ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler( 'ch8', @@ -166,6 +168,7 @@ void main() { final String result = await optionalMethodChannel.invokeMethod('sayHello', 'hello'); expect(result, isNull); }); + test('can handle method call with no registered plugin', () async { channel.setMethodCallHandler(null); final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); @@ -175,6 +178,7 @@ void main() { }); expect(envelope, isNull); }); + test('can handle method call of unimplemented method', () async { channel.setMethodCallHandler((MethodCall call) async { throw MissingPluginException(); @@ -186,6 +190,7 @@ void main() { }); expect(envelope, isNull); }); + test('can handle method call with successful result', () async { channel.setMethodCallHandler((MethodCall call) async => '${call.arguments}, world'); final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); @@ -195,6 +200,7 @@ void main() { }); expect(jsonMethod.decodeEnvelope(envelope), equals('hello, world')); }); + test('can handle method call with expressive error result', () async { channel.setMethodCallHandler((MethodCall call) async { throw PlatformException(code: 'bad', message: 'sayHello failed', details: null); @@ -214,6 +220,7 @@ void main() { fail('PlatformException expected'); } }); + test('can handle method call with other error result', () async { channel.setMethodCallHandler((MethodCall call) async { throw ArgumentError('bad'); @@ -233,6 +240,26 @@ void main() { fail('PlatformException expected'); } }); + + test('can check the handler', () { + Future handler(MethodCall call) => Future.value(null); + + const MethodChannel channel = MethodChannel('test_handler'); + expect(channel.checkMethodCallHandler(null), true); + expect(channel.checkMethodCallHandler(handler), false); + channel.setMethodCallHandler(handler); + expect(channel.checkMethodCallHandler(handler), true); + }); + + test('can check the mock handler', () { + Future handler(MethodCall call) => Future.value(null); + + const MethodChannel channel = MethodChannel('test_handler'); + expect(channel.checkMockMethodCallHandler(null), true); + expect(channel.checkMockMethodCallHandler(handler), false); + channel.setMockMethodCallHandler(handler); + expect(channel.checkMockMethodCallHandler(handler), true); + }); }); group('EventChannel', () { const MessageCodec jsonMessage = JSONMessageCodec(); diff --git a/packages/flutter/test/services/text_input_test.dart b/packages/flutter/test/services/text_input_test.dart index cb6020f6db..0ff58c1d4a 100644 --- a/packages/flutter/test/services/text_input_test.dart +++ b/packages/flutter/test/services/text_input_test.dart @@ -266,9 +266,16 @@ class FakeTextChannel implements MethodChannel { incoming = handler; } + @override + bool checkMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + + @override void setMockMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + @override + bool checkMockMethodCallHandler(Future Function(MethodCall call) handler) => throw UnimplementedError(); + void validateOutgoingMethodCalls(List calls) { expect(outgoingCalls.length, calls.length); bool hasError = false; diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index bd36580283..d8b98ddbe6 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -134,10 +134,20 @@ class TestDefaultBinaryMessenger extends BinaryMessenger { delegate.setMessageHandler(channel, handler); } + @override + bool checkMessageHandler(String channel, MessageHandler handler) { + return delegate.checkMessageHandler(channel, handler); + } + @override void setMockMessageHandler(String channel, MessageHandler handler) { delegate.setMockMessageHandler(channel, handler); } + + @override + bool checkMockMessageHandler(String channel, MessageHandler handler) { + return delegate.checkMockMessageHandler(channel, handler); + } } /// Base class for bindings used by widgets library tests. diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index 53725bf9c7..3ccd794181 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -52,20 +52,14 @@ class TestTextInput { register(); } /// Installs this object as a mock handler for [SystemChannels.textInput]. - void register() { - SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall); - _isRegistered = true; - } + void register() => SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall); /// Removes this object as a mock handler for [SystemChannels.textInput]. /// /// After calling this method, the channel will exchange messages with the /// Flutter engine. Use this with [FlutterDriver] tests that need to display /// on-screen keyboard provided by the operating system. - void unregister() { - SystemChannels.textInput.setMockMethodCallHandler(null); - _isRegistered = false; - } + void unregister() => SystemChannels.textInput.setMockMethodCallHandler(null); /// Log for method calls. /// @@ -76,12 +70,13 @@ class TestTextInput { /// Whether this [TestTextInput] is registered with [SystemChannels.textInput]. /// /// Use [register] and [unregister] methods to control this value. - // TODO(dnfield): This is unreliable. https://github.com/flutter/flutter/issues/47180 - bool get isRegistered => _isRegistered; - bool _isRegistered = false; + bool get isRegistered => SystemChannels.textInput.checkMockMethodCallHandler(_handleTextInputCall); /// Whether there are any active clients listening to text input. - bool get hasAnyClients => _client > 0; + bool get hasAnyClients { + assert(isRegistered); + return _client > 0; + } int _client = 0; @@ -122,11 +117,15 @@ class TestTextInput { } /// Whether the onscreen keyboard is visible to the user. - bool get isVisible => _isVisible; + bool get isVisible { + assert(isRegistered); + return _isVisible; + } bool _isVisible = false; /// Simulates the user changing the [TextEditingValue] to the given value. void updateEditingValue(TextEditingValue value) { + assert(isRegistered); // Not using the `expect` function because in the case of a FlutterDriver // test this code does not run in a package:test test zone. if (_client == 0) @@ -149,6 +148,7 @@ class TestTextInput { /// - User pressed the home button and sent the application to background. /// - User closed the virtual keyboard. void closeConnection() { + assert(isRegistered); // Not using the `expect` function because in the case of a FlutterDriver // test this code does not run in a package:test test zone. if (_client == 0) @@ -167,6 +167,7 @@ class TestTextInput { /// Simulates the user typing the given text. void enterText(String text) { + assert(isRegistered); updateEditingValue(TextEditingValue( text: text, )); @@ -176,6 +177,7 @@ class TestTextInput { /// Does not check that the [TextInputAction] performed is an acceptable one /// based on the `inputAction` [setClientArgs]. Future receiveAction(TextInputAction action) async { + assert(isRegistered); return TestAsyncUtils.guard(() { // Not using the `expect` function because in the case of a FlutterDriver // test this code does not run in a package:test test zone. @@ -215,6 +217,7 @@ class TestTextInput { /// Simulates the user hiding the onscreen keyboard. void hide() { + assert(isRegistered); _isVisible = false; } } diff --git a/packages/flutter_web_plugins/lib/src/plugin_registry.dart b/packages/flutter_web_plugins/lib/src/plugin_registry.dart index d7a3c7acee..88bac5e940 100644 --- a/packages/flutter_web_plugins/lib/src/plugin_registry.dart +++ b/packages/flutter_web_plugins/lib/src/plugin_registry.dart @@ -118,8 +118,7 @@ class _PlatformBinaryMessenger extends BinaryMessenger { } @override - void setMessageHandler( - String channel, Future Function(ByteData message) handler) { + void setMessageHandler(String channel, MessageHandler handler) { if (handler == null) _handlers.remove(channel); else @@ -129,12 +128,21 @@ class _PlatformBinaryMessenger extends BinaryMessenger { }); } + @override + bool checkMessageHandler(String channel, MessageHandler handler) => _handlers[channel] == handler; + @override void setMockMessageHandler( String channel, Future Function(ByteData message) handler) { throw FlutterError( 'Setting mock handlers is not supported on the platform side.'); } + + @override + bool checkMockMessageHandler(String channel, MessageHandler handler) { + throw FlutterError( + 'Setting mock handlers is not supported on the platform side.'); + } } /// The default [BinaryMessenger] for Flutter Web plugins.