Unverified Commit 5667b782 authored by adazh's avatar adazh Committed by GitHub

Added a Driver wait condition for no pending platform messages (#39196)

parent 6f71ce26
...@@ -201,6 +201,26 @@ class FirstFrameRasterized extends SerializableWaitCondition { ...@@ -201,6 +201,26 @@ class FirstFrameRasterized extends SerializableWaitCondition {
String get conditionName => 'FirstFrameRasterizedCondition'; String get conditionName => 'FirstFrameRasterizedCondition';
} }
/// A condition that waits until there are no pending platform messages.
class NoPendingPlatformMessages extends SerializableWaitCondition {
/// Creates a [NoPendingPlatformMessages] condition.
const NoPendingPlatformMessages();
/// Factory constructor to parse a [NoPendingPlatformMessages] instance from the
/// given JSON map.
///
/// The [json] argument must not be null.
factory NoPendingPlatformMessages.deserialize(Map<String, dynamic> json) {
assert(json != null);
if (json['conditionName'] != 'NoPendingPlatformMessagesCondition')
throw SerializationException('Error occurred during deserializing the NoPendingPlatformMessagesCondition JSON string: $json');
return const NoPendingPlatformMessages();
}
@override
String get conditionName => 'NoPendingPlatformMessagesCondition';
}
/// A combined condition that waits until all the given [conditions] are met. /// A combined condition that waits until all the given [conditions] are met.
class CombinedCondition extends SerializableWaitCondition { class CombinedCondition extends SerializableWaitCondition {
/// Creates a [CombinedCondition] condition. /// Creates a [CombinedCondition] condition.
...@@ -260,6 +280,8 @@ SerializableWaitCondition _deserialize(Map<String, dynamic> json) { ...@@ -260,6 +280,8 @@ SerializableWaitCondition _deserialize(Map<String, dynamic> json) {
return NoPendingFrame.deserialize(json); return NoPendingFrame.deserialize(json);
case 'FirstFrameRasterizedCondition': case 'FirstFrameRasterizedCondition':
return FirstFrameRasterized.deserialize(json); return FirstFrameRasterized.deserialize(json);
case 'NoPendingPlatformMessagesCondition':
return NoPendingPlatformMessages.deserialize(json);
case 'CombinedCondition': case 'CombinedCondition':
return CombinedCondition.deserialize(json); return CombinedCondition.deserialize(json);
} }
......
...@@ -56,6 +56,11 @@ class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, ...@@ -56,6 +56,11 @@ class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding,
callback: extension.call, callback: extension.call,
); );
} }
@override
BinaryMessenger createBinaryMessenger() {
return TestDefaultBinaryMessenger(super.createBinaryMessenger());
}
} }
/// Enables Flutter Driver VM service extension. /// Enables Flutter Driver VM service extension.
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../common/wait.dart'; import '../common/wait.dart';
...@@ -32,7 +34,7 @@ abstract class WaitCondition { ...@@ -32,7 +34,7 @@ abstract class WaitCondition {
/// A condition that waits until no transient callbacks are scheduled. /// A condition that waits until no transient callbacks are scheduled.
class _InternalNoTransientCallbacksCondition implements WaitCondition { class _InternalNoTransientCallbacksCondition implements WaitCondition {
/// Creates an [InternalNoTransientCallbacksCondition] instance. /// Creates an [_InternalNoTransientCallbacksCondition] instance.
const _InternalNoTransientCallbacksCondition(); const _InternalNoTransientCallbacksCondition();
/// Factory constructor to parse an [InternalNoTransientCallbacksCondition] /// Factory constructor to parse an [InternalNoTransientCallbacksCondition]
...@@ -60,7 +62,7 @@ class _InternalNoTransientCallbacksCondition implements WaitCondition { ...@@ -60,7 +62,7 @@ class _InternalNoTransientCallbacksCondition implements WaitCondition {
/// A condition that waits until no pending frame is scheduled. /// A condition that waits until no pending frame is scheduled.
class _InternalNoPendingFrameCondition implements WaitCondition { class _InternalNoPendingFrameCondition implements WaitCondition {
/// Creates an [InternalNoPendingFrameCondition] instance. /// Creates an [_InternalNoPendingFrameCondition] instance.
const _InternalNoPendingFrameCondition(); const _InternalNoPendingFrameCondition();
/// Factory constructor to parse an [InternalNoPendingFrameCondition] instance /// Factory constructor to parse an [InternalNoPendingFrameCondition] instance
...@@ -88,7 +90,7 @@ class _InternalNoPendingFrameCondition implements WaitCondition { ...@@ -88,7 +90,7 @@ class _InternalNoPendingFrameCondition implements WaitCondition {
/// A condition that waits until the Flutter engine has rasterized the first frame. /// A condition that waits until the Flutter engine has rasterized the first frame.
class _InternalFirstFrameRasterizedCondition implements WaitCondition { class _InternalFirstFrameRasterizedCondition implements WaitCondition {
/// Creates an [InternalFirstFrameRasterizedCondition] instance. /// Creates an [_InternalFirstFrameRasterizedCondition] instance.
const _InternalFirstFrameRasterizedCondition(); const _InternalFirstFrameRasterizedCondition();
/// Factory constructor to parse an [InternalNoPendingFrameCondition] instance /// Factory constructor to parse an [InternalNoPendingFrameCondition] instance
...@@ -112,16 +114,48 @@ class _InternalFirstFrameRasterizedCondition implements WaitCondition { ...@@ -112,16 +114,48 @@ class _InternalFirstFrameRasterizedCondition implements WaitCondition {
} }
} }
/// A condition that waits until no pending platform messages.
class _InternalNoPendingPlatformMessagesCondition implements WaitCondition {
/// Creates an [_InternalNoPendingPlatformMessagesCondition] instance.
const _InternalNoPendingPlatformMessagesCondition();
/// Factory constructor to parse an [_InternalNoPendingPlatformMessagesCondition] instance
/// from the given [SerializableWaitCondition] instance.
///
/// The [condition] argument must not be null.
factory _InternalNoPendingPlatformMessagesCondition.deserialize(SerializableWaitCondition condition) {
assert(condition != null);
if (condition.conditionName != 'NoPendingPlatformMessagesCondition')
throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}');
return const _InternalNoPendingPlatformMessagesCondition();
}
@override
bool get condition {
final TestDefaultBinaryMessenger binaryMessenger = ServicesBinding.instance.defaultBinaryMessenger;
return binaryMessenger.pendingMessageCount == 0;
}
@override
Future<void> wait() async {
final TestDefaultBinaryMessenger binaryMessenger = ServicesBinding.instance.defaultBinaryMessenger;
while (!condition) {
await binaryMessenger.platformMessagesFinished;
}
assert(condition);
}
}
/// A combined condition that waits until all the given [conditions] are met. /// A combined condition that waits until all the given [conditions] are met.
class _InternalCombinedCondition implements WaitCondition { class _InternalCombinedCondition implements WaitCondition {
/// Creates an [InternalCombinedCondition] instance with the given list of /// Creates an [_InternalCombinedCondition] instance with the given list of
/// [conditions]. /// [conditions].
/// ///
/// The [conditions] argument must not be null. /// The [conditions] argument must not be null.
const _InternalCombinedCondition(this.conditions) const _InternalCombinedCondition(this.conditions)
: assert(conditions != null); : assert(conditions != null);
/// Factory constructor to parse an [InternalCombinedCondition] instance from /// Factory constructor to parse an [_InternalCombinedCondition] instance from
/// the given [SerializableWaitCondition] instance. /// the given [SerializableWaitCondition] instance.
/// ///
/// The [condition] argument must not be null. /// The [condition] argument must not be null.
...@@ -173,6 +207,8 @@ WaitCondition deserializeCondition(SerializableWaitCondition waitCondition) { ...@@ -173,6 +207,8 @@ WaitCondition deserializeCondition(SerializableWaitCondition waitCondition) {
return _InternalNoPendingFrameCondition.deserialize(waitCondition); return _InternalNoPendingFrameCondition.deserialize(waitCondition);
case 'FirstFrameRasterizedCondition': case 'FirstFrameRasterizedCondition':
return _InternalFirstFrameRasterizedCondition.deserialize(waitCondition); return _InternalFirstFrameRasterizedCondition.deserialize(waitCondition);
case 'NoPendingPlatformMessagesCondition':
return _InternalNoPendingPlatformMessagesCondition.deserialize(waitCondition);
case 'CombinedCondition': case 'CombinedCondition':
return _InternalCombinedCondition.deserialize(waitCondition); return _InternalCombinedCondition.deserialize(waitCondition);
} }
......
...@@ -262,6 +262,18 @@ void main() { ...@@ -262,6 +262,18 @@ void main() {
await driver.waitForCondition(const NoPendingFrame(), timeout: _kTestTimeout); await driver.waitForCondition(const NoPendingFrame(), timeout: _kTestTimeout);
}); });
test('sends the wait for NoPendingPlatformMessages command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'waitForCondition',
'timeout': _kSerializedTestTimeout,
'conditionName': 'NoPendingPlatformMessagesCondition',
});
return makeMockResponse(<String, dynamic>{});
});
await driver.waitForCondition(const NoPendingPlatformMessages(), timeout: _kTestTimeout);
});
test('sends the waitForCondition of combined conditions command', () async { test('sends the waitForCondition of combined conditions command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{ expect(i.positionalArguments[1], <String, dynamic>{
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/common/diagnostics_tree.dart'; import 'package:flutter_driver/src/common/diagnostics_tree.dart';
...@@ -245,6 +246,207 @@ void main() { ...@@ -245,6 +246,207 @@ void main() {
}, },
); );
}); });
testWidgets(
'waiting for NoPendingPlatformMessages returns immediately when there\'re no platform messages', (WidgetTester tester) async {
extension
.call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
.then<void>(expectAsync1((Map<String, dynamic> r) {
result = r;
}));
await tester.idle();
expect(
result,
<String, dynamic>{
'isError': false,
'response': null,
},
);
});
testWidgets(
'waiting for NoPendingPlatformMessages returns until a single method channel call returns', (WidgetTester tester) async {
const MethodChannel channel = MethodChannel('helloChannel', JSONMethodCodec());
const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 10),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
channel.invokeMethod<String>('sayHello', 'hello');
extension
.call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
.then<void>(expectAsync1((Map<String, dynamic> r) {
result = r;
}));
// The channel message are delayed for 10 milliseconds, so nothing happens yet.
await tester.pump(const Duration(milliseconds: 5));
expect(result, isNull);
// Now we receive the result.
await tester.pump(const Duration(milliseconds: 5));
expect(
result,
<String, dynamic>{
'isError': false,
'response': null,
},
);
});
testWidgets(
'waiting for NoPendingPlatformMessages returns until both method channel calls return', (WidgetTester tester) async {
const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
// Configures channel 1
const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel1', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 10),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
// Configures channel 2
const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel2', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 20),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
channel1.invokeMethod<String>('sayHello', 'hello');
channel2.invokeMethod<String>('sayHello', 'hello');
extension
.call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
.then<void>(expectAsync1((Map<String, dynamic> r) {
result = r;
}));
// Neither of the channel responses is received, so nothing happens yet.
await tester.pump(const Duration(milliseconds: 5));
expect(result, isNull);
// Result of channel 1 is received, but channel 2 is still pending, so still waiting.
await tester.pump(const Duration(milliseconds: 10));
expect(result, isNull);
// Both of the results are received. Now we receive the result.
await tester.pump(const Duration(milliseconds: 30));
expect(
result,
<String, dynamic>{
'isError': false,
'response': null,
},
);
});
testWidgets(
'waiting for NoPendingPlatformMessages returns until new method channel call returns', (WidgetTester tester) async {
const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
// Configures channel 1
const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel1', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 10),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
// Configures channel 2
const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel2', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 20),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
channel1.invokeMethod<String>('sayHello', 'hello');
// Calls the waiting API before the second channel message is sent.
extension
.call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
.then<void>(expectAsync1((Map<String, dynamic> r) {
result = r;
}));
// The first channel message is not received, so nothing happens yet.
await tester.pump(const Duration(milliseconds: 5));
expect(result, isNull);
channel2.invokeMethod<String>('sayHello', 'hello');
// Result of channel 1 is received, but channel 2 is still pending, so still waiting.
await tester.pump(const Duration(milliseconds: 15));
expect(result, isNull);
// Both of the results are received. Now we receive the result.
await tester.pump(const Duration(milliseconds: 10));
expect(
result,
<String, dynamic>{
'isError': false,
'response': null,
},
);
});
testWidgets(
'waiting for NoPendingPlatformMessages returns until both old and new method channel calls return', (WidgetTester tester) async {
const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
// Configures channel 1
const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel1', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 20),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
// Configures channel 2
const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
'helloChannel2', (ByteData message) {
return Future<ByteData>.delayed(
const Duration(milliseconds: 10),
() => jsonMessage.encodeMessage(<dynamic>['hello world']));
});
channel1.invokeMethod<String>('sayHello', 'hello');
extension
.call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
.then<void>(expectAsync1((Map<String, dynamic> r) {
result = r;
}));
// The first channel message is not received, so nothing happens yet.
await tester.pump(const Duration(milliseconds: 5));
expect(result, isNull);
channel2.invokeMethod<String>('sayHello', 'hello');
// Result of channel 2 is received, but channel 1 is still pending, so still waiting.
await tester.pump(const Duration(milliseconds: 10));
expect(result, isNull);
// Now we receive the result.
await tester.pump(const Duration(milliseconds: 5));
expect(
result,
<String, dynamic>{
'isError': false,
'response': null,
},
);
});
}); });
group('getSemanticsId', () { group('getSemanticsId', () {
......
...@@ -80,6 +80,67 @@ enum TestBindingEventSource { ...@@ -80,6 +80,67 @@ enum TestBindingEventSource {
const Size _kDefaultTestViewportSize = Size(800.0, 600.0); const Size _kDefaultTestViewportSize = Size(800.0, 600.0);
/// A [BinaryMessenger] subclass that is used as the default binary messenger
/// under testing environment.
///
/// It tracks status of data sent across the Flutter platform barrier, which is
/// useful for testing frameworks to monitor and synchronize against the
/// platform messages.
class TestDefaultBinaryMessenger extends BinaryMessenger {
/// Creates a [TestDefaultBinaryMessenger] instance.
///
/// The [delegate] instance must not be null.
TestDefaultBinaryMessenger(this.delegate): assert(delegate != null);
/// The delegate [BinaryMessenger].
final BinaryMessenger delegate;
final List<Future<ByteData>> _pendingMessages = <Future<ByteData>>[];
/// The number of incomplete/pending calls sent to the platform channels.
int get pendingMessageCount => _pendingMessages.length;
@override
Future<ByteData> send(String channel, ByteData message) {
final Future<ByteData> resultFuture = delegate.send(channel, message);
// Removes the future itself from the [_pendingMessages] list when it
// completes.
if (resultFuture != null) {
_pendingMessages.add(resultFuture);
resultFuture.whenComplete(() => _pendingMessages.remove(resultFuture));
}
return resultFuture;
}
/// Returns a Future that completes after all the platform calls are finished.
///
/// If a new platform message is sent after this method is called, this new
/// message is not tracked. Use with [pendingMessageCount] to guarantee no
/// pending message calls.
Future<void> get platformMessagesFinished {
return Future.wait<void>(_pendingMessages);
}
@override
Future<void> handlePlatformMessage(
String channel,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) {
return delegate.handlePlatformMessage(channel, data, callback);
}
@override
void setMessageHandler(String channel, MessageHandler handler) {
delegate.setMessageHandler(channel, handler);
}
@override
void setMockMessageHandler(String channel, MessageHandler handler) {
delegate.setMockMessageHandler(channel, handler);
}
}
/// Base class for bindings used by widgets library tests. /// Base class for bindings used by widgets library tests.
/// ///
/// The [ensureInitialized] method creates (if necessary) and returns /// The [ensureInitialized] method creates (if necessary) and returns
...@@ -220,6 +281,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -220,6 +281,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// doesn't get generated for tests. // doesn't get generated for tests.
} }
@override
BinaryMessenger createBinaryMessenger() {
return TestDefaultBinaryMessenger(super.createBinaryMessenger());
}
/// Whether there is currently a test executing. /// Whether there is currently a test executing.
bool get inTest; bool get inTest;
......
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