1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/widgets.dart';
6library;
7
8import 'dart:convert';
9
10import 'message.dart';
11
12/// A Flutter Driver command that waits until a given [condition] is satisfied.
13class WaitForCondition extends Command {
14 /// Creates a command that waits for the given [condition] is met.
15 const WaitForCondition(this.condition, {super.timeout});
16
17 /// Deserializes this command from the value generated by [serialize].
18 WaitForCondition.deserialize(super.json) : condition = _deserialize(json), super.deserialize();
19
20 /// The condition that this command shall wait for.
21 final SerializableWaitCondition condition;
22
23 @override
24 Map<String, String> serialize() => super.serialize()..addAll(condition.serialize());
25
26 @override
27 String get kind => 'waitForCondition';
28
29 @override
30 bool get requiresRootWidgetAttached => condition.requiresRootWidgetAttached;
31}
32
33/// Thrown to indicate a serialization error.
34class SerializationException implements Exception {
35 /// Creates a [SerializationException] with an optional error message.
36 const SerializationException([this.message]);
37
38 /// The error message, possibly null.
39 final String? message;
40
41 @override
42 String toString() => 'SerializationException($message)';
43}
44
45/// Base class for Flutter Driver wait conditions, objects that describe conditions
46/// the driver can wait for.
47///
48/// This class is sent from the driver script running on the host to the driver
49/// extension on device to perform waiting on a given condition. In the extension,
50/// it will be converted to a `WaitCondition` that actually defines the wait logic.
51///
52/// If you subclass this, you also need to implement a `WaitCondition` in the extension.
53abstract class SerializableWaitCondition {
54 /// A const constructor to allow subclasses to be const.
55 const SerializableWaitCondition();
56
57 /// Identifies the name of the wait condition.
58 String get conditionName;
59
60 /// Serializes the object to JSON.
61 Map<String, String> serialize() {
62 return <String, String>{'conditionName': conditionName};
63 }
64
65 /// Whether this command requires the widget tree to be initialized before
66 /// the command may be run.
67 ///
68 /// This defaults to true to force the application under test to call [runApp]
69 /// before attempting to remotely drive the application. Subclasses may
70 /// override this to return false if they allow invocation before the
71 /// application has started.
72 ///
73 /// See also:
74 ///
75 /// * [WidgetsBinding.isRootWidgetAttached], which indicates whether the
76 /// widget tree has been initialized.
77 bool get requiresRootWidgetAttached => true;
78}
79
80/// A condition that waits until no transient callbacks are scheduled.
81class NoTransientCallbacks extends SerializableWaitCondition {
82 /// Creates a [NoTransientCallbacks] condition.
83 const NoTransientCallbacks();
84
85 /// Factory constructor to parse a [NoTransientCallbacks] instance from the
86 /// given JSON map.
87 factory NoTransientCallbacks.deserialize(Map<String, String> json) {
88 if (json['conditionName'] != 'NoTransientCallbacksCondition') {
89 throw SerializationException(
90 'Error occurred during deserializing the NoTransientCallbacksCondition JSON string: $json',
91 );
92 }
93 return const NoTransientCallbacks();
94 }
95
96 @override
97 String get conditionName => 'NoTransientCallbacksCondition';
98}
99
100/// A condition that waits until no pending frame is scheduled.
101class NoPendingFrame extends SerializableWaitCondition {
102 /// Creates a [NoPendingFrame] condition.
103 const NoPendingFrame();
104
105 /// Factory constructor to parse a [NoPendingFrame] instance from the given
106 /// JSON map.
107 factory NoPendingFrame.deserialize(Map<String, String> json) {
108 if (json['conditionName'] != 'NoPendingFrameCondition') {
109 throw SerializationException(
110 'Error occurred during deserializing the NoPendingFrameCondition JSON string: $json',
111 );
112 }
113 return const NoPendingFrame();
114 }
115
116 @override
117 String get conditionName => 'NoPendingFrameCondition';
118}
119
120/// A condition that waits until the Flutter engine has rasterized the first frame.
121class FirstFrameRasterized extends SerializableWaitCondition {
122 /// Creates a [FirstFrameRasterized] condition.
123 const FirstFrameRasterized();
124
125 /// Factory constructor to parse a [FirstFrameRasterized] instance from the
126 /// given JSON map.
127 factory FirstFrameRasterized.deserialize(Map<String, String> json) {
128 if (json['conditionName'] != 'FirstFrameRasterizedCondition') {
129 throw SerializationException(
130 'Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: $json',
131 );
132 }
133 return const FirstFrameRasterized();
134 }
135
136 @override
137 String get conditionName => 'FirstFrameRasterizedCondition';
138
139 @override
140 bool get requiresRootWidgetAttached => false;
141}
142
143/// A condition that waits until there are no pending platform messages.
144class NoPendingPlatformMessages extends SerializableWaitCondition {
145 /// Creates a [NoPendingPlatformMessages] condition.
146 const NoPendingPlatformMessages();
147
148 /// Factory constructor to parse a [NoPendingPlatformMessages] instance from the
149 /// given JSON map.
150 factory NoPendingPlatformMessages.deserialize(Map<String, String> json) {
151 if (json['conditionName'] != 'NoPendingPlatformMessagesCondition') {
152 throw SerializationException(
153 'Error occurred during deserializing the NoPendingPlatformMessagesCondition JSON string: $json',
154 );
155 }
156 return const NoPendingPlatformMessages();
157 }
158
159 @override
160 String get conditionName => 'NoPendingPlatformMessagesCondition';
161}
162
163/// A combined condition that waits until all the given [conditions] are met.
164class CombinedCondition extends SerializableWaitCondition {
165 /// Creates a [CombinedCondition] condition.
166 const CombinedCondition(this.conditions);
167
168 /// Factory constructor to parse a [CombinedCondition] instance from the
169 /// given JSON map.
170 factory CombinedCondition.deserialize(Map<String, String> jsonMap) {
171 if (jsonMap['conditionName'] != 'CombinedCondition') {
172 throw SerializationException(
173 'Error occurred during deserializing the CombinedCondition JSON string: $jsonMap',
174 );
175 }
176 if (jsonMap['conditions'] == null) {
177 return const CombinedCondition(<SerializableWaitCondition>[]);
178 }
179
180 final List<SerializableWaitCondition> conditions = <SerializableWaitCondition>[];
181 for (final Map<String, dynamic> condition
182 in (json.decode(jsonMap['conditions']!) as List<dynamic>).cast<Map<String, dynamic>>()) {
183 conditions.add(_deserialize(condition.cast<String, String>()));
184 }
185 return CombinedCondition(conditions);
186 }
187
188 /// A list of conditions it waits for.
189 final List<SerializableWaitCondition> conditions;
190
191 @override
192 String get conditionName => 'CombinedCondition';
193
194 @override
195 Map<String, String> serialize() {
196 final Map<String, String> jsonMap = super.serialize();
197 final List<Map<String, String>> jsonConditions = conditions.map((
198 SerializableWaitCondition condition,
199 ) {
200 return condition.serialize();
201 }).toList();
202 jsonMap['conditions'] = json.encode(jsonConditions);
203 return jsonMap;
204 }
205}
206
207/// Parses a [SerializableWaitCondition] or its subclass from the given [json] map.
208SerializableWaitCondition _deserialize(Map<String, String> json) {
209 return switch (json['conditionName']!) {
210 'NoTransientCallbacksCondition' => NoTransientCallbacks.deserialize(json),
211 'NoPendingFrameCondition' => NoPendingFrame.deserialize(json),
212 'FirstFrameRasterizedCondition' => FirstFrameRasterized.deserialize(json),
213 'NoPendingPlatformMessagesCondition' => NoPendingPlatformMessages.deserialize(json),
214 'CombinedCondition' => CombinedCondition.deserialize(json),
215 final String condition => throw SerializationException(
216 'Unsupported wait condition $condition in the JSON string $json',
217 ),
218 };
219}
220