From f28eb28f0db944c2c1237258ccc45f28d69c9a25 Mon Sep 17 00:00:00 2001 From: Rexios Date: Fri, 7 Apr 2023 12:12:27 -0400 Subject: [PATCH] [flutter_test] Adds method to mock EventChannels (#123726) [flutter_test] Adds method to mock EventChannels --- .../test/services/platform_channel_test.dart | 71 ++++++---------- packages/flutter_test/lib/flutter_test.dart | 1 + .../lib/src/mock_event_channel.dart | 79 +++++++++++++++++ .../src/test_default_binary_messenger.dart | 84 +++++++++++++++++++ .../test_default_binary_messenger_test.dart | 26 ++++++ 5 files changed, 216 insertions(+), 45 deletions(-) create mode 100644 packages/flutter_test/lib/src/mock_event_channel.dart diff --git a/packages/flutter/test/services/platform_channel_test.dart b/packages/flutter/test/services/platform_channel_test.dart index f3bda9ab272..144e9a3b769 100644 --- a/packages/flutter/test/services/platform_channel_test.dart +++ b/packages/flutter/test/services/platform_channel_test.dart @@ -265,69 +265,50 @@ void main() { }); group('EventChannel', () { - const MessageCodec jsonMessage = JSONMessageCodec(); const MethodCodec jsonMethod = JSONMethodCodec(); const EventChannel channel = EventChannel('ch', jsonMethod); - void emitEvent(ByteData? event) { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'ch', - event, - (ByteData? reply) {}, - ); - } + test('can receive event stream', () async { bool canceled = false; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler( - 'ch', - (ByteData? message) async { - final Map methodCall = jsonMessage.decodeMessage(message) as Map; - if (methodCall['method'] == 'listen') { - final String argument = methodCall['args'] as String; - emitEvent(jsonMethod.encodeSuccessEnvelope('${argument}1')); - emitEvent(jsonMethod.encodeSuccessEnvelope('${argument}2')); - emitEvent(null); - return jsonMethod.encodeSuccessEnvelope(null); - } else if (methodCall['method'] == 'cancel') { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler( + channel, + MockStreamHandler.inline( + onListen: (Object? arguments, MockStreamHandlerEventSink events) { + events.success('${arguments}1'); + events.success('${arguments}2'); + events.endOfStream(); + }, + onCancel: (Object? arguments) { canceled = true; - return jsonMethod.encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }, + }, + ), ); - final List events = await channel.receiveBroadcastStream('hello').toList(); + final List events = await channel.receiveBroadcastStream('hello').toList(); expect(events, orderedEquals(['hello1', 'hello2'])); await Future.delayed(Duration.zero); expect(canceled, isTrue); }); test('can receive error event', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler( - 'ch', - (ByteData? message) async { - final Map methodCall = jsonMessage.decodeMessage(message) as Map; - if (methodCall['method'] == 'listen') { - final String argument = methodCall['args'] as String; - emitEvent(jsonMethod.encodeErrorEnvelope(code: '404', message: 'Not Found.', details: argument)); - return jsonMethod.encodeSuccessEnvelope(null); - } else if (methodCall['method'] == 'cancel') { - return jsonMethod.encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }, + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler( + channel, + MockStreamHandler.inline( + onListen: (Object? arguments, MockStreamHandlerEventSink events) { + events.error(code: '404', message: 'Not Found.', details: arguments); + }, + ), ); - final List events = []; - final List errors = []; + final List events = []; + final List errors = []; channel.receiveBroadcastStream('hello').listen(events.add, onError: errors.add); await Future.delayed(Duration.zero); expect(events, isEmpty); expect(errors, hasLength(1)); expect(errors[0], isA()); - final PlatformException error = errors[0] as PlatformException; - expect(error.code, '404'); - expect(error.message, 'Not Found.'); - expect(error.details, 'hello'); + final PlatformException? error = errors[0] as PlatformException?; + expect(error?.code, '404'); + expect(error?.message, 'Not Found.'); + expect(error?.details, 'hello'); }); }); } diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart index 2767c589a3c..55e0d20ff16 100644 --- a/packages/flutter_test/lib/flutter_test.dart +++ b/packages/flutter_test/lib/flutter_test.dart @@ -69,6 +69,7 @@ export 'src/frame_timing_summarizer.dart'; export 'src/goldens.dart'; export 'src/image.dart'; export 'src/matchers.dart'; +export 'src/mock_event_channel.dart'; export 'src/nonconst.dart'; export 'src/platform.dart'; export 'src/restoration.dart'; diff --git a/packages/flutter_test/lib/src/mock_event_channel.dart b/packages/flutter_test/lib/src/mock_event_channel.dart new file mode 100644 index 00000000000..da7e46ffe88 --- /dev/null +++ b/packages/flutter_test/lib/src/mock_event_channel.dart @@ -0,0 +1,79 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +/// A mock stream handler for an [EventChannel] that mimics the native +/// StreamHandler API. +/// +/// The [onListen] callback is provided a [MockStreamHandlerEventSink] with +/// the following API: +/// - [MockStreamHandlerEventSink.success] sends a success event. +/// - [MockStreamHandlerEventSink.error] sends an error event. +/// - [MockStreamHandlerEventSink.endOfStream] sends an end of stream event. +abstract class MockStreamHandler { + /// Create a new [MockStreamHandler]. + MockStreamHandler(); + + /// Create a new inline [MockStreamHandler] with the given [onListen] and + /// [onCancel] handlers. + factory MockStreamHandler.inline({ + required MockStreamHandlerOnListenCallback onListen, + MockStreamHandlerOnCancelCallback? onCancel, + }) => _InlineMockStreamHandler(onListen: onListen, onCancel: onCancel); + + /// Handler for the listen event. + void onListen(Object? arguments, MockStreamHandlerEventSink events); + + /// Handler for the cancel event. + void onCancel(Object? arguments); +} + +/// Typedef for the inline onListen callback. +typedef MockStreamHandlerOnListenCallback = void Function(Object? arguments, MockStreamHandlerEventSink events); + +/// Typedef for the inline onCancel callback. +typedef MockStreamHandlerOnCancelCallback = void Function(Object? arguments); + +class _InlineMockStreamHandler extends MockStreamHandler { + _InlineMockStreamHandler({ + required MockStreamHandlerOnListenCallback onListen, + MockStreamHandlerOnCancelCallback? onCancel, + }) : _onListenInline = onListen, + _onCancelInline = onCancel; + + final MockStreamHandlerOnListenCallback _onListenInline; + final MockStreamHandlerOnCancelCallback? _onCancelInline; + + @override + void onListen(Object? arguments, MockStreamHandlerEventSink events) => _onListenInline(arguments, events); + + @override + void onCancel(Object? arguments) => _onCancelInline?.call(arguments); +} + +/// A mock event sink for a [MockStreamHandler] that mimics the native +/// [EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html) +/// API. +class MockStreamHandlerEventSink { + /// Create a new [MockStreamHandlerEventSink] with the given [sink]. + MockStreamHandlerEventSink(EventSink sink) : _sink = sink; + + final EventSink _sink; + + /// Send a success event. + void success(Object? event) => _sink.add(event); + + /// Send an error event. + void error({ + required String code, + String? message, + Object? details, + }) => _sink.addError(PlatformException(code: code, message: message, details: details)); + + /// Send an end of stream event. + void endOfStream() => _sink.close(); +} diff --git a/packages/flutter_test/lib/src/test_default_binary_messenger.dart b/packages/flutter_test/lib/src/test_default_binary_messenger.dart index 5d539b9fa5e..9b3f19dadf6 100644 --- a/packages/flutter_test/lib/src/test_default_binary_messenger.dart +++ b/packages/flutter_test/lib/src/test_default_binary_messenger.dart @@ -8,6 +8,9 @@ import 'dart:ui' as ui; import 'package:fake_async/fake_async.dart'; import 'package:flutter/services.dart'; +import 'mock_event_channel.dart'; +import 'widget_tester.dart'; + /// A function which takes the name of the method channel, it's handler, /// platform message and asynchronously returns an encoded response. typedef AllMessagesHandler = Future? Function( @@ -197,6 +200,9 @@ class TestDefaultBinaryMessenger extends BinaryMessenger { /// /// * [setMockMethodCallHandler], which wraps this method but decodes /// the messages using a [MethodCodec]. + /// + /// * [setMockStreamHandler], which wraps [setMockMethodCallHandler] to + /// handle [EventChannel] messages. void setMockMessageHandler(String channel, MessageHandler? handler, [ Object? identity ]) { if (handler == null) { _outboundHandlers.remove(channel); @@ -237,6 +243,9 @@ class TestDefaultBinaryMessenger extends BinaryMessenger { /// /// * [setMockMethodCallHandler], which is similar but decodes /// the messages using a [MethodCodec]. + /// + /// * [setMockStreamHandler], which wraps [setMockMethodCallHandler] to + /// handle [EventChannel] messages. void setMockDecodedMessageHandler(BasicMessageChannel channel, Future Function(T? message)? handler) { if (handler == null) { setMockMessageHandler(channel.name, null); @@ -302,6 +311,81 @@ class TestDefaultBinaryMessenger extends BinaryMessenger { }, handler); } + /// Set a handler for intercepting stream events sent to the + /// platform on the given channel. + /// + /// Intercepted method calls are not forwarded to the platform. + /// + /// The given handler will replace the currently registered + /// handler for that channel, if any. To stop intercepting messages + /// at all, pass null as the handler. + /// + /// Events are decoded using the codec of the channel. + /// + /// The handler's stream messages are used as a response, after encoding + /// them using the channel's codec. + /// + /// To send an error, pass the error information to the handler's event sink. + /// + /// {@macro flutter.flutter_test.TestDefaultBinaryMessenger.handlePlatformMessage.asyncHandlers} + /// + /// Registered handlers are cleared after each test. + /// + /// See also: + /// + /// * [setMockMethodCallHandler], which is the similar method for + /// [MethodChannel]. + /// + /// * [setMockMessageHandler], which is similar but provides raw + /// access to the underlying bytes. + /// + /// * [setMockDecodedMessageHandler], which is similar but decodes + /// the messages using a [MessageCodec]. + void setMockStreamHandler(EventChannel channel, MockStreamHandler? handler) { + if (handler == null) { + setMockMessageHandler(channel.name, null); + return; + } + + final StreamController controller = StreamController(); + addTearDown(controller.close); + + setMockMethodCallHandler(MethodChannel(channel.name, channel.codec), (MethodCall call) async { + switch (call.method) { + case 'listen': + return handler.onListen(call.arguments, MockStreamHandlerEventSink(controller.sink)); + case 'cancel': + return handler.onCancel(call.arguments); + default: + throw UnimplementedError('Method ${call.method} not implemented'); + } + }); + + final StreamSubscription sub = controller.stream.listen( + (Object? e) => channel.binaryMessenger.handlePlatformMessage( + channel.name, + channel.codec.encodeSuccessEnvelope(e), + null, + ), + ); + addTearDown(sub.cancel); + sub.onError((Object? e) { + if (e is! PlatformException) { + throw ArgumentError('Stream error must be a PlatformException'); + } + channel.binaryMessenger.handlePlatformMessage( + channel.name, + channel.codec.encodeErrorEnvelope( + code: e.code, + message: e.message, + details: e.details, + ), + null, + ); + }); + sub.onDone(() => channel.binaryMessenger.handlePlatformMessage(channel.name, null, null)); + } + /// Returns true if the `handler` argument matches the `handler` /// previously passed to [setMockMessageHandler], /// [setMockDecodedMessageHandler], or [setMockMethodCallHandler]. diff --git a/packages/flutter_test/test/test_default_binary_messenger_test.dart b/packages/flutter_test/test/test_default_binary_messenger_test.dart index 82d2e4c4c26..4426a6b282b 100644 --- a/packages/flutter_test/test/test_default_binary_messenger_test.dart +++ b/packages/flutter_test/test/test_default_binary_messenger_test.dart @@ -69,6 +69,32 @@ void main() { expect(result?.buffer.asUint8List(), Uint8List.fromList([2, 3, 4])); }); + test('Mock StreamHandler is set correctly', () async { + const EventChannel channel = EventChannel(''); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler( + channel, + MockStreamHandler.inline(onListen: (Object? arguments, MockStreamHandlerEventSink events) { + events.success(arguments); + events.error(code: 'code', message: 'message', details: 'details'); + events.endOfStream(); + }) + ); + + expect( + channel.receiveBroadcastStream('argument'), + emitsInOrder([ + 'argument', + emitsError( + isA() + .having((PlatformException e) => e.code, 'code', 'code') + .having((PlatformException e) => e.message, 'message', 'message') + .having((PlatformException e) => e.details, 'details', 'details'), + ), + emitsDone, + ]), + ); + }); + testWidgets('Mock AllMessagesHandler is set correctly', (WidgetTester tester) async { final TestDefaultBinaryMessenger binaryMessenger =