wait.dart 8.58 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13
// 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.
14 15
  const WaitForCondition(this.condition, {super.timeout})
      : assert(condition != null);
16 17 18 19

  /// Deserializes this command from the value generated by [serialize].
  ///
  /// The [json] argument cannot be null.
20
  WaitForCondition.deserialize(super.json)
21 22
      : assert(json != null),
        condition = _deserialize(json),
23
        super.deserialize();
24 25 26 27 28 29 30 31 32

  /// 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';
33 34 35

  @override
  bool get requiresRootWidgetAttached => condition.requiresRootWidgetAttached;
36 37 38 39 40 41 42 43
}

/// 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.
44
  final String? message;
45 46 47 48 49 50 51 52 53 54

  @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,
55
/// it will be converted to a `WaitCondition` that actually defines the wait logic.
56
///
57
/// If you subclass this, you also need to implement a `WaitCondition` in the extension.
58 59 60 61 62 63 64 65 66 67
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>{
68
      'conditionName': conditionName,
69 70
    };
  }
71 72 73 74 75 76 77 78 79 80 81 82 83 84

  /// 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;
85 86 87 88 89 90 91 92 93 94 95
}

/// 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.
96
  factory NoTransientCallbacks.deserialize(Map<String, String> json) {
97
    assert(json != null);
98
    if (json['conditionName'] != 'NoTransientCallbacksCondition') {
99
      throw SerializationException('Error occurred during deserializing the NoTransientCallbacksCondition JSON string: $json');
100
    }
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    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.
117
  factory NoPendingFrame.deserialize(Map<String, String> json) {
118
    assert(json != null);
119
    if (json['conditionName'] != 'NoPendingFrameCondition') {
120
      throw SerializationException('Error occurred during deserializing the NoPendingFrameCondition JSON string: $json');
121
    }
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    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.
138
  factory FirstFrameRasterized.deserialize(Map<String, String> json) {
139
    assert(json != null);
140
    if (json['conditionName'] != 'FirstFrameRasterizedCondition') {
141
      throw SerializationException('Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: $json');
142
    }
143 144 145 146 147
    return const FirstFrameRasterized();
  }

  @override
  String get conditionName => 'FirstFrameRasterizedCondition';
148 149 150

  @override
  bool get requiresRootWidgetAttached => false;
151 152
}

153 154 155 156 157 158 159 160 161
/// 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.
162
  factory NoPendingPlatformMessages.deserialize(Map<String, String> json) {
163
    assert(json != null);
164
    if (json['conditionName'] != 'NoPendingPlatformMessagesCondition') {
165
      throw SerializationException('Error occurred during deserializing the NoPendingPlatformMessagesCondition JSON string: $json');
166
    }
167 168 169 170 171 172 173
    return const NoPendingPlatformMessages();
  }

  @override
  String get conditionName => 'NoPendingPlatformMessagesCondition';
}

174 175 176 177 178 179 180 181 182 183 184 185
/// 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.
186
  factory CombinedCondition.deserialize(Map<String, String> jsonMap) {
187
    assert(jsonMap != null);
188
    if (jsonMap['conditionName'] != 'CombinedCondition') {
189
      throw SerializationException('Error occurred during deserializing the CombinedCondition JSON string: $jsonMap');
190
    }
191 192 193 194 195
    if (jsonMap['conditions'] == null) {
      return const CombinedCondition(<SerializableWaitCondition>[]);
    }

    final List<SerializableWaitCondition> conditions = <SerializableWaitCondition>[];
196
    for (final Map<String, dynamic> condition in (json.decode(jsonMap['conditions']!) as List<dynamic>).cast<Map<String, dynamic>>()) {
197
      conditions.add(_deserialize(condition.cast<String, String>()));
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    }
    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.
224
SerializableWaitCondition _deserialize(Map<String, String> json) {
225
  assert(json != null);
226
  final String conditionName = json['conditionName']!;
227 228 229 230 231 232 233
  switch (conditionName) {
    case 'NoTransientCallbacksCondition':
      return NoTransientCallbacks.deserialize(json);
    case 'NoPendingFrameCondition':
      return NoPendingFrame.deserialize(json);
    case 'FirstFrameRasterizedCondition':
      return FirstFrameRasterized.deserialize(json);
234 235
    case 'NoPendingPlatformMessagesCondition':
      return NoPendingPlatformMessages.deserialize(json);
236 237 238 239 240 241
    case 'CombinedCondition':
      return CombinedCondition.deserialize(json);
  }
  throw SerializationException(
      'Unsupported wait condition $conditionName in the JSON string $json');
}