// 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:convert'; 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, {super.timeout}) : assert(condition != null); /// Deserializes this command from the value generated by [serialize]. /// /// The [json] argument cannot be null. WaitForCondition.deserialize(super.json) : assert(json != null), condition = _deserialize(json), super.deserialize(); /// The condition that this command shall wait for. final SerializableWaitCondition condition; @override Map<String, String> serialize() => super.serialize()..addAll(condition.serialize()); @override String get kind => 'waitForCondition'; @override bool get requiresRootWidgetAttached => condition.requiresRootWidgetAttached; } /// Thrown to indicate a 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)'; } /// Base class for Flutter Driver wait conditions, objects that describe conditions /// the driver can wait for. /// /// This class is sent from the driver script running on the host to the driver /// extension on device to perform waiting on a given condition. In the extension, /// it will be converted to a `WaitCondition` that actually defines the wait logic. /// /// If you subclass this, you also need to implement a `WaitCondition` in the extension. abstract class SerializableWaitCondition { /// A const constructor to allow subclasses to be const. const SerializableWaitCondition(); /// Identifies the name of the wait condition. String get conditionName; /// Serializes the object to JSON. Map<String, String> serialize() { return <String, String>{ 'conditionName': conditionName, }; } /// Whether this command requires the widget tree to be initialized before /// the command may be run. /// /// This defaults to true to force the application under test to call [runApp] /// before attempting to remotely drive the application. Subclasses may /// override this to return false if they allow invocation before the /// application has started. /// /// See also: /// /// * [WidgetsBinding.isRootWidgetAttached], which indicates whether the /// widget tree has been initialized. bool get requiresRootWidgetAttached => true; } /// A condition that waits until no transient callbacks are scheduled. class NoTransientCallbacks extends SerializableWaitCondition { /// Creates a [NoTransientCallbacks] condition. const NoTransientCallbacks(); /// Factory constructor to parse a [NoTransientCallbacks] instance from the /// given JSON map. /// /// The [json] argument must not be null. factory NoTransientCallbacks.deserialize(Map<String, String> json) { assert(json != null); if (json['conditionName'] != 'NoTransientCallbacksCondition') { throw SerializationException('Error occurred during deserializing the NoTransientCallbacksCondition JSON string: $json'); } return const NoTransientCallbacks(); } @override String get conditionName => 'NoTransientCallbacksCondition'; } /// A condition that waits until no pending frame is scheduled. class NoPendingFrame extends SerializableWaitCondition { /// Creates a [NoPendingFrame] condition. const NoPendingFrame(); /// Factory constructor to parse a [NoPendingFrame] instance from the given /// JSON map. /// /// The [json] argument must not be null. factory NoPendingFrame.deserialize(Map<String, String> json) { assert(json != null); if (json['conditionName'] != 'NoPendingFrameCondition') { throw SerializationException('Error occurred during deserializing the NoPendingFrameCondition JSON string: $json'); } return const NoPendingFrame(); } @override String get conditionName => 'NoPendingFrameCondition'; } /// A condition that waits until the Flutter engine has rasterized the first frame. class FirstFrameRasterized extends SerializableWaitCondition { /// Creates a [FirstFrameRasterized] condition. const FirstFrameRasterized(); /// Factory constructor to parse a [FirstFrameRasterized] instance from the /// given JSON map. /// /// The [json] argument must not be null. factory FirstFrameRasterized.deserialize(Map<String, String> json) { assert(json != null); if (json['conditionName'] != 'FirstFrameRasterizedCondition') { throw SerializationException('Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: $json'); } return const FirstFrameRasterized(); } @override String get conditionName => 'FirstFrameRasterizedCondition'; @override bool get requiresRootWidgetAttached => false; } /// 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, String> 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. class CombinedCondition extends SerializableWaitCondition { /// Creates a [CombinedCondition] condition. /// /// 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<String, String> 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(<SerializableWaitCondition>[]); } final List<SerializableWaitCondition> conditions = <SerializableWaitCondition>[]; for (final Map<String, dynamic> condition in (json.decode(jsonMap['conditions']!) as List<dynamic>).cast<Map<String, dynamic>>()) { conditions.add(_deserialize(condition.cast<String, String>())); } return CombinedCondition(conditions); } /// A list of conditions it waits for. final List<SerializableWaitCondition> conditions; @override String get conditionName => 'CombinedCondition'; @override Map<String, String> serialize() { final Map<String, String> jsonMap = super.serialize(); final List<Map<String, String>> jsonConditions = conditions.map( (SerializableWaitCondition condition) { assert(condition != null); return condition.serialize(); }).toList(); jsonMap['conditions'] = json.encode(jsonConditions); return jsonMap; } } /// Parses a [SerializableWaitCondition] or its subclass from the given [json] map. /// /// The [json] argument must not be null. SerializableWaitCondition _deserialize(Map<String, String> json) { assert(json != null); final String conditionName = json['conditionName']!; switch (conditionName) { case 'NoTransientCallbacksCondition': return NoTransientCallbacks.deserialize(json); case 'NoPendingFrameCondition': return NoPendingFrame.deserialize(json); case 'FirstFrameRasterizedCondition': return FirstFrameRasterized.deserialize(json); case 'NoPendingPlatformMessagesCondition': return NoPendingPlatformMessages.deserialize(json); case 'CombinedCondition': return CombinedCondition.deserialize(json); } throw SerializationException( 'Unsupported wait condition $conditionName in the JSON string $json'); }