diff --git a/packages/flutter_driver/lib/src/common/find.dart b/packages/flutter_driver/lib/src/common/find.dart index 7fd3ad97d9a..3adfc4d7b11 100644 --- a/packages/flutter_driver/lib/src/common/find.dart +++ b/packages/flutter_driver/lib/src/common/find.dart @@ -106,52 +106,6 @@ class WaitForAbsentResult extends Result { Map toJson() => {}; } -/// A Flutter Driver command that waits until there are no more transient callbacks in the queue. -class WaitUntilNoTransientCallbacks extends Command { - /// Creates a command that waits for there to be no transient callbacks. - const WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout); - - /// Deserializes this command from the value generated by [serialize]. - WaitUntilNoTransientCallbacks.deserialize(Map json) - : super.deserialize(json); - - @override - String get kind => 'waitUntilNoTransientCallbacks'; -} - -/// A Flutter Driver command that waits until the frame is synced. -class WaitUntilNoPendingFrame extends Command { - /// Creates a command that waits until there's no pending frame scheduled. - const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); - - /// Deserializes this command from the value generated by [serialize]. - WaitUntilNoPendingFrame.deserialize(Map json) - : super.deserialize(json); - - @override - String get kind => 'waitUntilNoPendingFrame'; -} - -/// A Flutter Driver command that waits until the Flutter engine rasterizes the -/// first frame. -/// -/// {@template flutter.frame_rasterized_vs_presented} -/// Usually, the time that a frame is rasterized is very close to the time that -/// it gets presented on the display. Specifically, rasterization is the last -/// expensive phase of a frame that's still in Flutter's control. -/// {@endtemplate} -class WaitUntilFirstFrameRasterized extends Command { - /// Creates this command. - const WaitUntilFirstFrameRasterized({ Duration timeout }) : super(timeout: timeout); - - /// Deserializes this command from the value generated by [serialize]. - WaitUntilFirstFrameRasterized.deserialize(Map json) - : super.deserialize(json); - - @override - String get kind => 'waitUntilFirstFrameRasterized'; -} - /// Base class for Flutter Driver finders, objects that describe how the driver /// should search for elements. abstract class SerializableFinder { diff --git a/packages/flutter_driver/lib/src/common/wait.dart b/packages/flutter_driver/lib/src/common/wait.dart new file mode 100644 index 00000000000..25419eb52c5 --- /dev/null +++ b/packages/flutter_driver/lib/src/common/wait.dart @@ -0,0 +1,324 @@ +// Copyright 2019 The Chromium 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 'dart:convert'; + +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +import 'message.dart'; + +/// A Flutter Driver command that waits until a given [condition] is satisfied. +class WaitForCondition extends Command { + /// Creates a command that waits for the given [condition] is met. + /// + /// The [condition] argument must not be null. + const WaitForCondition(this.condition, {Duration timeout}) + : assert(condition != null), + super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + /// + /// The [json] argument cannot be null. + WaitForCondition.deserialize(Map json) + : assert(json != null), + condition = _deserialize(json), + super.deserialize(json); + + /// The condition that this command shall wait for. + final WaitCondition condition; + + @override + Map serialize() => super.serialize()..addAll(condition.serialize()); + + @override + String get kind => 'waitForCondition'; +} + +/// A Flutter Driver command that waits until there are no more transient callbacks in the queue. +/// +/// This command has been deprecated in favor of [WaitForCondition]. Construct +/// a command that waits until no transient callbacks as follows: +/// +/// ```dart +/// WaitForCondition noTransientCallbacks = WaitForCondition(NoTransientCallbacksCondition()); +/// ``` +@Deprecated('This command has been deprecated in favor of WaitForCondition. ' + 'Use WaitForCondition command with NoTransientCallbacksCondition.') +class WaitUntilNoTransientCallbacks extends Command { + /// Creates a command that waits for there to be no transient callbacks. + const WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + WaitUntilNoTransientCallbacks.deserialize(Map json) + : super.deserialize(json); + + @override + String get kind => 'waitUntilNoTransientCallbacks'; +} + +/// A Flutter Driver command that waits until the frame is synced. +/// +/// This command has been deprecated in favor of [WaitForCondition]. Construct +/// a command that waits until no pending frame as follows: +/// +/// ```dart +/// WaitForCondition noPendingFrame = WaitForCondition(NoPendingFrameCondition()); +/// ``` +@Deprecated('This command has been deprecated in favor of WaitForCondition. ' + 'Use WaitForCondition command with NoPendingFrameCondition.') +class WaitUntilNoPendingFrame extends Command { + /// Creates a command that waits until there's no pending frame scheduled. + const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + WaitUntilNoPendingFrame.deserialize(Map json) + : super.deserialize(json); + + @override + String get kind => 'waitUntilNoPendingFrame'; +} + +/// A Flutter Driver command that waits until the Flutter engine rasterizes the +/// first frame. +/// +/// {@template flutter.frame_rasterized_vs_presented} +/// Usually, the time that a frame is rasterized is very close to the time that +/// it gets presented on the display. Specifically, rasterization is the last +/// expensive phase of a frame that's still in Flutter's control. +/// {@endtemplate} +/// +/// This command has been deprecated in favor of [WaitForCondition]. Construct +/// a command that waits until no pending frame as follows: +/// +/// ```dart +/// WaitForCondition firstFrameRasterized = WaitForCondition(FirstFrameRasterizedCondition()); +/// ``` +@Deprecated('This command has been deprecated in favor of WaitForCondition. ' + 'Use WaitForCondition command with FirstFrameRasterizedCondition.') +class WaitUntilFirstFrameRasterized extends Command { + /// Creates this command. + const WaitUntilFirstFrameRasterized({ Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + WaitUntilFirstFrameRasterized.deserialize(Map json) + : super.deserialize(json); + + @override + String get kind => 'waitUntilFirstFrameRasterized'; +} + +/// Base class for a condition that can be waited upon. +abstract class WaitCondition { + /// Gets the current status of the [condition], executed in the context of the + /// Flutter app: + /// + /// * True, if the condition is satisfied. + /// * False otherwise. + /// + /// The future returned by [wait] will complete when this [condition] is + /// fulfilled. + bool get condition; + + /// Returns a future that completes when [condition] turns true. + Future wait(); + + /// Serializes the object to JSON. + Map serialize(); +} + +/// Thrown to indicate a JSON serialization error. +class SerializationException implements Exception { + /// Creates a [SerializationException] with an optional error message. + const SerializationException([this.message]); + + /// The error message, possibly null. + final String message; + + @override + String toString() => 'SerializationException($message)'; +} + +/// A condition that waits until no transient callbacks are scheduled. +class NoTransientCallbacksCondition implements WaitCondition { + /// Creates a [NoTransientCallbacksCondition] instance. + const NoTransientCallbacksCondition(); + + /// Factory constructor to parse a [NoTransientCallbacksCondition] instance + /// from the given JSON map. + /// + /// The [json] argument must not be null. + factory NoTransientCallbacksCondition.deserialize(Map json) { + assert(json != null); + if (json['conditionName'] != 'NoTransientCallbacksCondition') + throw SerializationException('Error occurred during deserializing the NoTransientCallbacksCondition JSON string: $json'); + return const NoTransientCallbacksCondition(); + } + + @override + bool get condition => SchedulerBinding.instance.transientCallbackCount == 0; + + @override + Future wait() async { + while (!condition) { + await SchedulerBinding.instance.endOfFrame; + } + assert(condition); + } + + @override + Map serialize() { + return { + 'conditionName': 'NoTransientCallbacksCondition', + }; + } +} + +/// A condition that waits until no pending frame is scheduled. +class NoPendingFrameCondition implements WaitCondition { + /// Creates a [NoPendingFrameCondition] instance. + const NoPendingFrameCondition(); + + /// Factory constructor to parse a [NoPendingFrameCondition] instance from the + /// given JSON map. + /// + /// The [json] argument must not be null. + factory NoPendingFrameCondition.deserialize(Map json) { + assert(json != null); + if (json['conditionName'] != 'NoPendingFrameCondition') + throw SerializationException('Error occurred during deserializing the NoPendingFrameCondition JSON string: $json'); + return const NoPendingFrameCondition(); + } + + @override + bool get condition => !SchedulerBinding.instance.hasScheduledFrame; + + @override + Future wait() async { + while (!condition) { + await SchedulerBinding.instance.endOfFrame; + } + assert(condition); + } + + @override + Map serialize() { + return { + 'conditionName': 'NoPendingFrameCondition', + }; + } +} + +/// A condition that waits until the Flutter engine has rasterized the first frame. +class FirstFrameRasterizedCondition implements WaitCondition { + /// Creates a [FirstFrameRasterizedCondition] instance. + const FirstFrameRasterizedCondition(); + + /// Factory constructor to parse a [NoPendingFrameCondition] instance from the + /// given JSON map. + /// + /// The [json] argument must not be null. + factory FirstFrameRasterizedCondition.deserialize(Map json) { + assert(json != null); + if (json['conditionName'] != 'FirstFrameRasterizedCondition') + throw SerializationException('Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: $json'); + return const FirstFrameRasterizedCondition(); + } + + @override + bool get condition => WidgetsBinding.instance.firstFrameRasterized; + + @override + Future wait() async { + await WidgetsBinding.instance.waitUntilFirstFrameRasterized; + assert(condition); + } + + @override + Map serialize() { + return { + 'conditionName': 'FirstFrameRasterizedCondition', + }; + } +} + +/// A combined condition that waits until all the given [conditions] are met. +class CombinedCondition implements WaitCondition { + /// Creates a [CombinedCondition] instance with the given list of + /// [conditions]. + /// + /// The [conditions] argument must not be null. + const CombinedCondition(this.conditions) + : assert(conditions != null); + + /// Factory constructor to parse a [CombinedCondition] instance from the given + /// JSON map. + /// + /// The [jsonMap] argument must not be null. + factory CombinedCondition.deserialize(Map jsonMap) { + assert(jsonMap != null); + if (jsonMap['conditionName'] != 'CombinedCondition') + throw SerializationException('Error occurred during deserializing the CombinedCondition JSON string: $jsonMap'); + if (jsonMap['conditions'] == null) { + return const CombinedCondition([]); + } + + final List conditions = []; + for (Map condition in json.decode(jsonMap['conditions'])) { + conditions.add(_deserialize(condition)); + } + return CombinedCondition(conditions); + } + + /// A list of conditions it waits for. + final List conditions; + + @override + bool get condition { + return conditions.every((WaitCondition condition) => condition.condition); + } + + @override + Future wait() async { + while (!condition) { + for (WaitCondition condition in conditions) { + assert (condition != null); + await condition.wait(); + } + } + assert(condition); + } + + @override + Map serialize() { + final Map jsonMap = { + 'conditionName': 'CombinedCondition' + }; + final List> jsonConditions = conditions.map( + (WaitCondition condition) { + assert(condition != null); + return condition.serialize(); + }).toList(); + jsonMap['conditions'] = json.encode(jsonConditions); + return jsonMap; + } +} + +/// Parses a [WaitCondition] or its subclass from the given [json] map. +/// +/// The [json] argument cannot be null. +WaitCondition _deserialize(Map json) { + assert(json != null); + final String conditionName = json['conditionName']; + switch (conditionName) { + case 'NoTransientCallbacksCondition': + return NoTransientCallbacksCondition.deserialize(json); + case 'NoPendingFrameCondition': + return NoPendingFrameCondition.deserialize(json); + case 'CombinedCondition': + return CombinedCondition.deserialize(json); + } + throw SerializationException('Unsupported wait condition $conditionName in the JSON string $json'); +} \ No newline at end of file diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index 5d7eec8dba8..953a84914e6 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -28,6 +28,7 @@ import '../common/render_tree.dart'; import '../common/request_data.dart'; import '../common/semantics.dart'; import '../common/text.dart'; +import '../common/wait.dart'; import 'common.dart'; import 'timeline.dart'; @@ -491,12 +492,17 @@ class FlutterDriver { await _sendCommand(WaitForAbsent(finder, timeout: timeout)); } + /// Waits until the given [waitCondition] is satisfied. + Future waitForCondition(WaitCondition waitCondition, {Duration timeout}) async { + await _sendCommand(WaitForCondition(waitCondition, timeout: timeout)); + } + /// Waits until there are no more transient callbacks in the queue. /// /// Use this method when you need to wait for the moment when the application /// becomes "stable", for example, prior to taking a [screenshot]. Future waitUntilNoTransientCallbacks({ Duration timeout }) async { - await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout)); + await _sendCommand(WaitForCondition(const NoTransientCallbacksCondition(), timeout: timeout)); } /// Waits until the next [Window.onReportTimings] is called. @@ -504,7 +510,7 @@ class FlutterDriver { /// Use this method to wait for the first frame to be rasterized during the /// app launch. Future waitUntilFirstFrameRasterized() async { - await _sendCommand(const WaitUntilFirstFrameRasterized()); + await _sendCommand(const WaitForCondition(FirstFrameRasterizedCondition())); } Future _getOffset(SerializableFinder finder, OffsetType type, { Duration timeout }) async { diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index c204d2f4f69..d7b32d49dc3 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -29,6 +29,7 @@ import '../common/render_tree.dart'; import '../common/request_data.dart'; import '../common/semantics.dart'; import '../common/text.dart'; +import '../common/wait.dart'; const String _extensionMethodName = 'driver'; const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; @@ -112,9 +113,10 @@ class FlutterDriverExtension { 'tap': _tap, 'waitFor': _waitFor, 'waitForAbsent': _waitForAbsent, - 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, - 'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, - 'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, + 'waitForCondition': _waitForCondition, + 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, // ignore: deprecated_member_use_from_same_package + 'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, // ignore: deprecated_member_use_from_same_package + 'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, // ignore: deprecated_member_use_from_same_package 'get_semantics_id': _getSemanticsId, 'get_offset': _getOffset, 'get_diagnostics_tree': _getDiagnosticsTree, @@ -134,9 +136,10 @@ class FlutterDriverExtension { 'tap': (Map params) => Tap.deserialize(params), 'waitFor': (Map params) => WaitFor.deserialize(params), 'waitForAbsent': (Map params) => WaitForAbsent.deserialize(params), - 'waitUntilNoTransientCallbacks': (Map params) => WaitUntilNoTransientCallbacks.deserialize(params), - 'waitUntilNoPendingFrame': (Map params) => WaitUntilNoPendingFrame.deserialize(params), - 'waitUntilFirstFrameRasterized': (Map params) => WaitUntilFirstFrameRasterized.deserialize(params), + 'waitForCondition': (Map params) => WaitForCondition.deserialize(params), + 'waitUntilNoTransientCallbacks': (Map params) => WaitUntilNoTransientCallbacks.deserialize(params), // ignore: deprecated_member_use_from_same_package + 'waitUntilNoPendingFrame': (Map params) => WaitUntilNoPendingFrame.deserialize(params), // ignore: deprecated_member_use_from_same_package + 'waitUntilFirstFrameRasterized': (Map params) => WaitUntilFirstFrameRasterized.deserialize(params), // ignore: deprecated_member_use_from_same_package 'get_semantics_id': (Map params) => GetSemanticsId.deserialize(params), 'get_offset': (Map params) => GetOffset.deserialize(params), 'get_diagnostics_tree': (Map params) => GetDiagnosticsTree.deserialize(params), @@ -223,6 +226,7 @@ class FlutterDriverExtension { } // This can be used to wait for the first frame being rasterized during app launch. + @Deprecated('This method has been deprecated in favor of _waitForCondition.') Future _waitUntilFirstFrameRasterized(Command command) async { await WidgetsBinding.instance.waitUntilFirstFrameRasterized; return null; @@ -370,6 +374,14 @@ class FlutterDriverExtension { return const WaitForAbsentResult(); } + Future _waitForCondition(Command command) async { + assert(command != null); + final WaitForCondition waitForConditionCommand = command; + await waitForConditionCommand.condition.wait(); + return null; + } + + @Deprecated('This method has been deprecated in favor of _waitForCondition.') Future _waitUntilNoTransientCallbacks(Command command) async { if (SchedulerBinding.instance.transientCallbackCount != 0) await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); @@ -393,6 +405,9 @@ class FlutterDriverExtension { /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more /// details on how to do this. Note, disabling frame sync will require the /// test author to use some other method to avoid flakiness. + /// + /// This method has been deprecated in favor of [_waitForCondition]. + @Deprecated('This method has been deprecated in favor of _waitForCondition.') Future _waitUntilNoPendingFrame(Command command) async { await _waitUntilFrame(() { return SchedulerBinding.instance.transientCallbackCount == 0 diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index 0ecf0361655..896940b30af 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter_driver/src/common/error.dart'; import 'package:flutter_driver/src/common/health.dart'; +import 'package:flutter_driver/src/common/wait.dart'; import 'package:flutter_driver/src/driver/driver.dart'; import 'package:flutter_driver/src/driver/timeline.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; @@ -248,12 +249,41 @@ void main() { }); }); + group('waitForCondition', () { + test('sends the wait for NoPendingFrameCondition command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'waitForCondition', + 'timeout': _kSerializedTestTimeout, + 'conditionName': 'NoPendingFrameCondition', + }); + return makeMockResponse({}); + }); + await driver.waitForCondition(const NoPendingFrameCondition(), timeout: _kTestTimeout); + }); + + test('sends the waitForCondition of combined conditions command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'waitForCondition', + 'timeout': _kSerializedTestTimeout, + 'conditionName': 'CombinedCondition', + 'conditions': '[{"conditionName":"NoPendingFrameCondition"},{"conditionName":"NoTransientCallbacksCondition"}]', + }); + return makeMockResponse({}); + }); + const WaitCondition combinedCondition = CombinedCondition([NoPendingFrameCondition(), NoTransientCallbacksCondition()]); + await driver.waitForCondition(combinedCondition, timeout: _kTestTimeout); + }); + }); + group('waitUntilNoTransientCallbacks', () { test('sends the waitUntilNoTransientCallbacks command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { - 'command': 'waitUntilNoTransientCallbacks', + 'command': 'waitForCondition', 'timeout': _kSerializedTestTimeout, + 'conditionName': 'NoTransientCallbacksCondition', }); return makeMockResponse({}); }); diff --git a/packages/flutter_driver/test/src/extension_test.dart b/packages/flutter_driver/test/src/extension_test.dart index 9b54d954f1d..535510aee01 100644 --- a/packages/flutter_driver/test/src/extension_test.dart +++ b/packages/flutter_driver/test/src/extension_test.dart @@ -12,6 +12,7 @@ import 'package:flutter_driver/src/common/find.dart'; import 'package:flutter_driver/src/common/geometry.dart'; import 'package:flutter_driver/src/common/request_data.dart'; import 'package:flutter_driver/src/common/text.dart'; +import 'package:flutter_driver/src/common/wait.dart'; import 'package:flutter_driver/src/extension/extension.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -28,18 +29,18 @@ void main() { }); testWidgets('returns immediately when transient callback queue is empty', (WidgetTester tester) async { - extension.call(const WaitUntilNoTransientCallbacks().serialize()) - .then(expectAsync1((Map r) { - result = r; - })); + extension.call(const WaitUntilNoTransientCallbacks().serialize()) // ignore: deprecated_member_use_from_same_package + .then(expectAsync1((Map r) { + result = r; + })); await tester.idle(); expect( - result, - { - 'isError': false, - 'response': null, - }, + result, + { + 'isError': false, + 'response': null, + }, ); }); @@ -48,10 +49,10 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoTransientCallbacks().serialize()) - .then(expectAsync1((Map r) { - result = r; - })); + extension.call(const WaitUntilNoTransientCallbacks().serialize()) // ignore: deprecated_member_use_from_same_package + .then(expectAsync1((Map r) { + result = r; + })); // Nothing should happen until the next frame. await tester.idle(); @@ -60,11 +61,11 @@ void main() { // NOW we should receive the result. await tester.pump(); expect( - result, - { - 'isError': false, - 'response': null, - }, + result, + { + 'isError': false, + 'response': null, + }, ); }); @@ -76,6 +77,173 @@ void main() { }); }); + group('waitForCondition', () { + FlutterDriverExtension extension; + Map result; + int messageId = 0; + final List log = []; + + setUp(() { + result = null; + extension = FlutterDriverExtension((String message) async { log.add(message); return (messageId += 1).toString(); }, false); + }); + + testWidgets('waiting for NoTransientCallbacksCondition returns immediately when transient callback queue is empty', (WidgetTester tester) async { + extension.call(const WaitForCondition(NoTransientCallbacksCondition()).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + await tester.idle(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets('waiting for NoTransientCallbacksCondition returns until no transient callbacks', (WidgetTester tester) async { + SchedulerBinding.instance.scheduleFrameCallback((_) { + // Intentionally blank. We only care about existence of a callback. + }); + + extension.call(const WaitForCondition(NoTransientCallbacksCondition()).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + // Nothing should happen until the next frame. + await tester.idle(); + expect(result, isNull); + + // NOW we should receive the result. + await tester.pump(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets('waiting for NoPendingFrameCondition returns immediately when frame is synced', ( + WidgetTester tester) async { + extension.call(const WaitForCondition(NoPendingFrameCondition()).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + await tester.idle(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets('waiting for NoPendingFrameCondition returns until no pending scheduled frame', (WidgetTester tester) async { + SchedulerBinding.instance.scheduleFrame(); + + extension.call(const WaitForCondition(NoPendingFrameCondition()).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + // Nothing should happen until the next frame. + await tester.idle(); + expect(result, isNull); + + // NOW we should receive the result. + await tester.pump(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets( + 'waiting for combined conditions returns immediately', (WidgetTester tester) async { + const WaitCondition combinedCondition = CombinedCondition([NoTransientCallbacksCondition(), NoPendingFrameCondition()]); + extension.call(const WaitForCondition(combinedCondition).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + await tester.idle(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets( + 'waiting for combined conditions returns until no transient callbacks', (WidgetTester tester) async { + SchedulerBinding.instance.scheduleFrame(); + SchedulerBinding.instance.scheduleFrameCallback((_) { + // Intentionally blank. We only care about existence of a callback. + }); + + const WaitCondition combinedCondition = CombinedCondition([NoTransientCallbacksCondition(), NoPendingFrameCondition()]); + extension.call(const WaitForCondition(combinedCondition).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + // Nothing should happen until the next frame. + await tester.idle(); + expect(result, isNull); + + // NOW we should receive the result. + await tester.pump(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + + testWidgets( + 'waiting for combined conditions returns until no pending scheduled frame', (WidgetTester tester) async { + SchedulerBinding.instance.scheduleFrame(); + SchedulerBinding.instance.scheduleFrameCallback((_) { + // Intentionally blank. We only care about existence of a callback. + }); + + const WaitCondition combinedCondition = CombinedCondition([NoPendingFrameCondition(), NoTransientCallbacksCondition()]); + extension.call(const WaitForCondition(combinedCondition).serialize()) + .then(expectAsync1((Map r) { + result = r; + })); + + // Nothing should happen until the next frame. + await tester.idle(); + expect(result, isNull); + + // NOW we should receive the result. + await tester.pump(); + expect( + result, + { + 'isError': false, + 'response': null, + }, + ); + }); + }); + group('getSemanticsId', () { FlutterDriverExtension extension; setUp(() { @@ -345,7 +513,7 @@ void main() { testWidgets('returns immediately when frame is synced', ( WidgetTester tester) async { - extension.call(const WaitUntilNoPendingFrame().serialize()) + extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package .then(expectAsync1((Map r) { result = r; })); @@ -366,7 +534,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoPendingFrame().serialize()) + extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package .then(expectAsync1((Map r) { result = r; })); @@ -390,7 +558,7 @@ void main() { 'waits until no pending scheduled frame', (WidgetTester tester) async { SchedulerBinding.instance.scheduleFrame(); - extension.call(const WaitUntilNoPendingFrame().serialize()) + extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package .then(expectAsync1((Map r) { result = r; })); diff --git a/packages/flutter_driver/test/src/wait_test.dart b/packages/flutter_driver/test/src/wait_test.dart new file mode 100644 index 00000000000..c15a3cd2e5e --- /dev/null +++ b/packages/flutter_driver/test/src/wait_test.dart @@ -0,0 +1,202 @@ +// Copyright 2019 The Chromium 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 'package:flutter_driver/src/common/wait.dart'; + +import '../common.dart'; + +void main() { + group('WaitForCondition', () { + test('WaitForCondition serialize', () { + expect( + const WaitForCondition(NoTransientCallbacksCondition()).serialize(), + {'command': 'waitForCondition', 'conditionName': 'NoTransientCallbacksCondition'}); + }); + + test('WaitForCondition serialize with timeout', () { + expect( + const WaitForCondition(NoTransientCallbacksCondition(), timeout: Duration(milliseconds: 10)).serialize(), + {'command': 'waitForCondition', 'timeout': '10', 'conditionName': 'NoTransientCallbacksCondition'}); + }); + + test('WaitForCondition deserialize', () { + final Map jsonMap = { + 'command': 'waitForCondition', + 'conditionName': 'NoTransientCallbacksCondition', + }; + final WaitForCondition waitForCondition = WaitForCondition.deserialize(jsonMap); + expect(waitForCondition.kind, 'waitForCondition'); + expect(waitForCondition.condition, equals(const NoTransientCallbacksCondition())); + }); + + test('WaitForCondition deserialize with timeout', () { + final Map jsonMap = { + 'command': 'waitForCondition', + 'timeout': '10', + 'conditionName': 'NoTransientCallbacksCondition', + }; + final WaitForCondition waitForCondition = WaitForCondition.deserialize(jsonMap); + expect(waitForCondition.kind, 'waitForCondition'); + expect(waitForCondition.condition, equals(const NoTransientCallbacksCondition())); + expect(waitForCondition.timeout, equals(const Duration(milliseconds: 10))); + }); + }); + + group('NoTransientCallbacksCondition', () { + test('NoTransientCallbacksCondition serialize', () { + expect( + const NoTransientCallbacksCondition().serialize(), + {'conditionName': 'NoTransientCallbacksCondition'}); + }); + + test('NoTransientCallbacksCondition deserialize', () { + final Map jsonMap = { + 'conditionName': 'NoTransientCallbacksCondition', + }; + final NoTransientCallbacksCondition condition = + NoTransientCallbacksCondition.deserialize(jsonMap); + expect(condition, equals(const NoTransientCallbacksCondition())); + expect(condition.serialize(), equals(jsonMap)); + }); + + test('NoTransientCallbacksCondition deserialize error', () { + expect( + () => NoTransientCallbacksCondition.deserialize({'conditionName': 'Unknown'}), + throwsA(predicate((SerializationException e) => + e.message == 'Error occurred during deserializing the NoTransientCallbacksCondition JSON string: {conditionName: Unknown}'))); + }); + }); + + group('NoPendingFrameCondition', () { + test('NoPendingFrameCondition serialize', () { + expect(const NoPendingFrameCondition().serialize(), { + 'conditionName': 'NoPendingFrameCondition', + }); + }); + + test('NoPendingFrameCondition deserialize', () { + final Map jsonMap = { + 'conditionName': 'NoPendingFrameCondition', + }; + final NoPendingFrameCondition condition = + NoPendingFrameCondition.deserialize(jsonMap); + expect(condition, equals(const NoPendingFrameCondition())); + expect(condition.serialize(), equals(jsonMap)); + }); + + test('NoPendingFrameCondition deserialize error', () { + expect( + () => NoPendingFrameCondition.deserialize({'conditionName': 'Unknown'}), + throwsA(predicate((SerializationException e) => + e.message == 'Error occurred during deserializing the NoPendingFrameCondition JSON string: {conditionName: Unknown}'))); + }); + }); + + group('FirstFrameRasterizedCondition', () { + test('FirstFrameRasterizedCondition serialize', () { + expect( + const FirstFrameRasterizedCondition().serialize(), + {'conditionName': 'FirstFrameRasterizedCondition'}); + }); + + test('FirstFrameRasterizedCondition deserialize', () { + final Map jsonMap = { + 'conditionName': 'FirstFrameRasterizedCondition', + }; + final FirstFrameRasterizedCondition condition = + FirstFrameRasterizedCondition.deserialize(jsonMap); + expect(condition, equals(const FirstFrameRasterizedCondition())); + expect(condition.serialize(), equals(jsonMap)); + }); + + test('FirstFrameRasterizedCondition deserialize error', () { + expect( + () => FirstFrameRasterizedCondition.deserialize({'conditionName': 'Unknown'}), + throwsA(predicate((SerializationException e) => + e.message == 'Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: {conditionName: Unknown}'))); + }); + }); + + group('CombinedCondition', () { + test('CombinedCondition serialize', () { + const CombinedCondition combinedCondition = + CombinedCondition([ + NoTransientCallbacksCondition(), + NoPendingFrameCondition() + ]); + + expect(combinedCondition.serialize(), { + 'conditionName': 'CombinedCondition', + 'conditions': + '[{"conditionName":"NoTransientCallbacksCondition"},{"conditionName":"NoPendingFrameCondition"}]', + }); + }); + + test('CombinedCondition serialize - empty condition list', () { + const CombinedCondition combinedCondition = CombinedCondition([]); + + expect(combinedCondition.serialize(), { + 'conditionName': 'CombinedCondition', + 'conditions': '[]', + }); + }); + + test('CombinedCondition deserialize - empty condition list', () { + final Map jsonMap = { + 'conditionName': 'CombinedCondition', + 'conditions': '[]', + }; + final CombinedCondition condition = CombinedCondition.deserialize(jsonMap); + expect(condition.conditions, equals([])); + expect(condition.serialize(), equals(jsonMap)); + }); + + test('CombinedCondition deserialize', () { + final Map jsonMap = { + 'conditionName': 'CombinedCondition', + 'conditions': + '[{"conditionName":"NoPendingFrameCondition"},{"conditionName":"NoTransientCallbacksCondition"}]', + }; + final CombinedCondition condition = + CombinedCondition.deserialize(jsonMap); + expect( + condition.conditions, + equals([ + const NoPendingFrameCondition(), + const NoTransientCallbacksCondition(), + ])); + expect(condition.serialize(), jsonMap); + }); + + test('CombinedCondition deserialize - no condition list', () { + final CombinedCondition condition = + CombinedCondition.deserialize({'conditionName': 'CombinedCondition',}); + expect(condition.conditions, equals([])); + expect(condition.serialize(), { + 'conditionName': 'CombinedCondition', + 'conditions': '[]', + }); + }); + + test('CombinedCondition deserialize error', () { + expect( + () => CombinedCondition.deserialize({'conditionName': 'Unknown'}), + throwsA(predicate((SerializationException e) => + e.message == 'Error occurred during deserializing the CombinedCondition JSON string: {conditionName: Unknown}'))); + }); + + test('CombinedCondition deserialize error - Unknown condition type', () { + expect( + () { + return CombinedCondition.deserialize({ + 'conditionName': 'CombinedCondition', + 'conditions': + '[{"conditionName":"UnknownCondition"},{"conditionName":"NoTransientCallbacksCondition"}]', + }); + }, + throwsA(predicate((SerializationException e) => + e.message == 'Unsupported wait condition UnknownCondition in the JSON string {conditionName: UnknownCondition}'))); + }); + }); +}