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:fake_async/fake_async.dart'; |
6 | /// @docImport 'package:flutter/material.dart'; |
7 | /// @docImport 'package:matcher/matcher.dart'; |
8 | /// @docImport 'package:test_api/hooks.dart'; |
9 | library; |
10 | |
11 | import 'package:flutter/cupertino.dart'; |
12 | import 'package:flutter/foundation.dart'; |
13 | import 'package:flutter/gestures.dart'; |
14 | import 'package:flutter/material.dart' show Tooltip; |
15 | import 'package:flutter/rendering.dart'; |
16 | import 'package:flutter/scheduler.dart'; |
17 | import 'package:flutter/services.dart'; |
18 | import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart' ; |
19 | import 'package:matcher/expect.dart' as matcher_expect; |
20 | import 'package:meta/meta.dart' ; |
21 | import 'package:test_api/scaffolding.dart' as test_package; |
22 | |
23 | import 'binding.dart'; |
24 | import 'controller.dart'; |
25 | import 'finders.dart'; |
26 | import 'matchers.dart'; |
27 | import 'restoration.dart'; |
28 | import 'test_async_utils.dart'; |
29 | import 'test_compat.dart'; |
30 | import 'test_pointer.dart'; |
31 | import 'test_text_input.dart'; |
32 | import 'tree_traversal.dart'; |
33 | |
34 | // Keep users from needing multiple imports to test semantics. |
35 | export 'package:flutter/rendering.dart' show SemanticsHandle; |
36 | // We re-export the matcher package minus some features that we reimplement. |
37 | // |
38 | // - expect is reimplemented below, to catch incorrect async usage. |
39 | // |
40 | // - isInstanceOf is reimplemented in matchers.dart because we don't want to |
41 | // mark it as deprecated (ours is just a method, not a class). |
42 | // |
43 | export 'package:matcher/expect.dart' hide expect, isInstanceOf; |
44 | // We re-export the test package minus some features that we reimplement. |
45 | // |
46 | // Specifically: |
47 | // |
48 | // - test, group, setUpAll, tearDownAll, setUp, tearDown, and expect would |
49 | // conflict with our own implementations in test_compat.dart. This handles |
50 | // setting up a declarer when one is not defined, which can happen when a |
51 | // test is executed via `flutter run`. |
52 | // |
53 | // The test_api package has a deprecation warning to discourage direct use but |
54 | // that doesn't apply here. |
55 | export 'package:test_api/hooks.dart' show TestFailure; |
56 | export 'package:test_api/scaffolding.dart' |
57 | show |
58 | OnPlatform, |
59 | Retry, |
60 | Skip, |
61 | Tags, |
62 | TestOn, |
63 | Timeout, |
64 | addTearDown, |
65 | markTestSkipped, |
66 | printOnFailure, |
67 | pumpEventQueue, |
68 | registerException, |
69 | spawnHybridCode, |
70 | spawnHybridUri; |
71 | |
72 | /// Signature for callback to [testWidgets] and [benchmarkWidgets]. |
73 | typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester); |
74 | |
75 | // Return the last element that satisfies `test`, or return null if not found. |
76 | E? _lastWhereOrNull<E>(Iterable<E> list, bool Function(E) test) { |
77 | late E result; |
78 | bool foundMatching = false; |
79 | for (final E element in list) { |
80 | if (test(element)) { |
81 | result = element; |
82 | foundMatching = true; |
83 | } |
84 | } |
85 | if (foundMatching) { |
86 | return result; |
87 | } |
88 | return null; |
89 | } |
90 | |
91 | // Examples can assume: |
92 | // typedef MyWidget = Placeholder; |
93 | |
94 | /// Runs the [callback] inside the Flutter test environment. |
95 | /// |
96 | /// Use this function for testing custom [StatelessWidget]s and |
97 | /// [StatefulWidget]s. |
98 | /// |
99 | /// The callback can be asynchronous (using `async`/`await` or |
100 | /// using explicit [Future]s). |
101 | /// |
102 | /// The `timeout` argument specifies the backstop timeout implemented by the |
103 | /// `test` package. If set, it should be relatively large (minutes). It defaults |
104 | /// to ten minutes for tests run by `flutter test`, and is unlimited for tests |
105 | /// run by `flutter run`; specifically, it defaults to |
106 | /// [TestWidgetsFlutterBinding.defaultTestTimeout]. |
107 | /// |
108 | /// If the `semanticsEnabled` parameter is set to `true`, |
109 | /// [WidgetTester.ensureSemantics] will have been called before the tester is |
110 | /// passed to the `callback`, and that handle will automatically be disposed |
111 | /// after the callback is finished. It defaults to true. |
112 | /// |
113 | /// This function uses the [test] function in the test package to |
114 | /// register the given callback as a test. The callback, when run, |
115 | /// will be given a new instance of [WidgetTester]. The [find] object |
116 | /// provides convenient widget [Finder]s for use with the |
117 | /// [WidgetTester]. |
118 | /// |
119 | /// When the [variant] argument is set, [testWidgets] will run the test once for |
120 | /// each value of the [TestVariant.values]. If [variant] is not set, the test |
121 | /// will be run once using the base test environment. |
122 | /// |
123 | /// If the [tags] are passed, they declare user-defined tags that are implemented by |
124 | /// the `test` package. |
125 | /// |
126 | /// The argument [experimentalLeakTesting] is experimental and is not recommended |
127 | /// for use outside of the Flutter framework. |
128 | /// When [experimentalLeakTesting] is set, it is used to leak track objects created |
129 | /// during test execution. |
130 | /// Otherwise [LeakTesting.settings] is used. |
131 | /// Adjust [LeakTesting.settings] in `flutter_test_config.dart` |
132 | /// (see https://flutter.dev/to/flutter-test-docs) |
133 | /// for the entire package or folder, or in the test's main for a test file |
134 | /// (don't use [setUp] or [setUpAll]). |
135 | /// To turn off leak tracking just for one test, set [experimentalLeakTesting] to |
136 | /// `LeakTrackingForTests.ignore()`. |
137 | /// |
138 | /// ## Sample code |
139 | /// |
140 | /// ```dart |
141 | /// testWidgets('MyWidget', (WidgetTester tester) async { |
142 | /// await tester.pumpWidget(const MyWidget()); |
143 | /// await tester.tap(find.text('Save')); |
144 | /// expect(find.text('Success'), findsOneWidget); |
145 | /// }); |
146 | /// ``` |
147 | @isTest |
148 | void testWidgets( |
149 | String description, |
150 | WidgetTesterCallback callback, { |
151 | bool? skip, |
152 | test_package.Timeout? timeout, |
153 | bool semanticsEnabled = true, |
154 | TestVariant<Object?> variant = const DefaultTestVariant(), |
155 | dynamic tags, |
156 | int? retry, |
157 | LeakTesting? experimentalLeakTesting, |
158 | }) { |
159 | assert( |
160 | variant.values.isNotEmpty, |
161 | 'There must be at least one value to test in the testing variant.' , |
162 | ); |
163 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
164 | final WidgetTester tester = WidgetTester._(binding); |
165 | for (final dynamic value in variant.values) { |
166 | final String variationDescription = variant.describeValue(value); |
167 | // IDEs may make assumptions about the format of this suffix in order to |
168 | // support running tests directly from the editor (where they may have |
169 | // access to only the test name, provided by the analysis server). |
170 | // See https://github.com/flutter/flutter/issues/86659. |
171 | final String combinedDescription = variationDescription.isNotEmpty |
172 | ? ' $description (variant: $variationDescription)' |
173 | : description; |
174 | test( |
175 | combinedDescription, |
176 | () { |
177 | tester._testDescription = combinedDescription; |
178 | SemanticsHandle? semanticsHandle; |
179 | tester._recordNumberOfSemanticsHandles(); |
180 | if (semanticsEnabled) { |
181 | semanticsHandle = tester.ensureSemantics(); |
182 | } |
183 | test_package.addTearDown(binding.postTest); |
184 | return binding.runTest( |
185 | () async { |
186 | debugResetSemanticsIdCounter(); |
187 | Object? memento; |
188 | try { |
189 | memento = await variant.setUp(value); |
190 | binding.reset(); // TODO(ianh): the binding should just do this itself in _runTest |
191 | maybeSetupLeakTrackingForTest(experimentalLeakTesting, combinedDescription); |
192 | await callback(tester); |
193 | } finally { |
194 | await variant.tearDown(value, memento); |
195 | maybeTearDownLeakTrackingForTest(); |
196 | } |
197 | semanticsHandle?.dispose(); |
198 | }, |
199 | tester._endOfTestVerifications, |
200 | description: combinedDescription, |
201 | ); |
202 | }, |
203 | skip: skip, |
204 | timeout: timeout ?? binding.defaultTestTimeout, |
205 | tags: tags, |
206 | retry: retry, |
207 | ); |
208 | } |
209 | } |
210 | |
211 | /// An abstract base class for describing test environment variants. |
212 | /// |
213 | /// These serve as elements of the `variants` argument to [testWidgets]. |
214 | /// |
215 | /// Use care when adding more testing variants: it multiplies the number of |
216 | /// tests which run. This can drastically increase the time it takes to run all |
217 | /// the tests. |
218 | abstract class TestVariant<T> { |
219 | /// A const constructor so that subclasses can be const. |
220 | const TestVariant(); |
221 | |
222 | /// Returns an iterable of the variations that this test dimension represents. |
223 | /// |
224 | /// The variations returned should be unique so that the same variation isn't |
225 | /// needlessly run twice. |
226 | Iterable<T> get values; |
227 | |
228 | /// Returns the string that will be used to both add to the test description, and |
229 | /// be printed when a test fails for this variation. |
230 | String describeValue(T value); |
231 | |
232 | /// A function that will be called before each value is tested, with the |
233 | /// value that will be tested. |
234 | /// |
235 | /// This function should preserve any state needed to restore the testing |
236 | /// environment back to its base state when [tearDown] is called in the |
237 | /// `Object` that is returned. The returned object will then be passed to |
238 | /// [tearDown] as a `memento` when the test is complete. |
239 | Future<Object?> setUp(T value); |
240 | |
241 | /// A function that is guaranteed to be called after a value is tested, even |
242 | /// if it throws an exception. |
243 | /// |
244 | /// Calling this function must return the testing environment back to the base |
245 | /// state it was in before [setUp] was called. The [memento] is the object |
246 | /// returned from [setUp] when it was called. |
247 | Future<void> tearDown(T value, covariant Object? memento); |
248 | } |
249 | |
250 | /// The [TestVariant] that represents the "default" test that is run if no |
251 | /// `variants` iterable is specified for [testWidgets]. |
252 | /// |
253 | /// This variant can be added into a list of other test variants to provide |
254 | /// a "control" test where nothing is changed from the base test environment. |
255 | class DefaultTestVariant extends TestVariant<void> { |
256 | /// A const constructor for a [DefaultTestVariant]. |
257 | const DefaultTestVariant(); |
258 | |
259 | @override |
260 | Iterable<void> get values => const <void>[null]; |
261 | |
262 | @override |
263 | String describeValue(void value) => '' ; |
264 | |
265 | @override |
266 | Future<void> setUp(void value) async {} |
267 | |
268 | @override |
269 | Future<void> tearDown(void value, void memento) async {} |
270 | } |
271 | |
272 | /// A [TestVariant] that runs tests with [debugDefaultTargetPlatformOverride] |
273 | /// set to different values of [TargetPlatform]. |
274 | class TargetPlatformVariant extends TestVariant<TargetPlatform> { |
275 | /// Creates a [TargetPlatformVariant] that tests the given [values]. |
276 | const TargetPlatformVariant(this.values); |
277 | |
278 | /// Creates a [TargetPlatformVariant] that tests all values from |
279 | /// the [TargetPlatform] enum. If [excluding] is provided, will test all platforms |
280 | /// except those in [excluding]. |
281 | TargetPlatformVariant.all({Set<TargetPlatform> excluding = const <TargetPlatform>{}}) |
282 | : values = TargetPlatform.values.toSet()..removeAll(excluding); |
283 | |
284 | /// Creates a [TargetPlatformVariant] that includes platforms that are |
285 | /// considered desktop platforms. |
286 | TargetPlatformVariant.desktop() |
287 | : values = <TargetPlatform>{TargetPlatform.linux, TargetPlatform.macOS, TargetPlatform.windows}; |
288 | |
289 | /// Creates a [TargetPlatformVariant] that includes platforms that are |
290 | /// considered mobile platforms. |
291 | TargetPlatformVariant.mobile() |
292 | : values = <TargetPlatform>{TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.fuchsia}; |
293 | |
294 | /// Creates a [TargetPlatformVariant] that tests only the given value of |
295 | /// [TargetPlatform]. |
296 | TargetPlatformVariant.only(TargetPlatform platform) : values = <TargetPlatform>{platform}; |
297 | |
298 | @override |
299 | final Set<TargetPlatform> values; |
300 | |
301 | @override |
302 | String describeValue(TargetPlatform value) => value.toString(); |
303 | |
304 | @override |
305 | Future<TargetPlatform?> setUp(TargetPlatform value) async { |
306 | final TargetPlatform? previousTargetPlatform = debugDefaultTargetPlatformOverride; |
307 | debugDefaultTargetPlatformOverride = value; |
308 | return previousTargetPlatform; |
309 | } |
310 | |
311 | @override |
312 | Future<void> tearDown(TargetPlatform value, TargetPlatform? memento) async { |
313 | debugDefaultTargetPlatformOverride = memento; |
314 | } |
315 | } |
316 | |
317 | /// A [TestVariant] that runs separate tests with each of the given values. |
318 | /// |
319 | /// To use this variant, define it before the test, and then access |
320 | /// [currentValue] inside the test. |
321 | /// |
322 | /// The values are typically enums, but they don't have to be. The `toString` |
323 | /// for the given value will be used to describe the variant. Values will have |
324 | /// their type name stripped from their `toString` output, so that enum values |
325 | /// will only print the value, not the type. |
326 | /// |
327 | /// {@tool snippet} |
328 | /// This example shows how to set up the test to access the [currentValue]. In |
329 | /// this example, two tests will be run, one with `value1`, and one with |
330 | /// `value2`. The test with `value2` will fail. The names of the tests will be: |
331 | /// |
332 | /// - `Test handling of TestScenario (value1)` |
333 | /// - `Test handling of TestScenario (value2)` |
334 | /// |
335 | /// ```dart |
336 | /// enum TestScenario { |
337 | /// value1, |
338 | /// value2, |
339 | /// value3, |
340 | /// } |
341 | /// |
342 | /// final ValueVariant<TestScenario> variants = ValueVariant<TestScenario>( |
343 | /// <TestScenario>{TestScenario.value1, TestScenario.value2}, |
344 | /// ); |
345 | /// void main() { |
346 | /// testWidgets('Test handling of TestScenario', (WidgetTester tester) async { |
347 | /// expect(variants.currentValue, equals(TestScenario.value1)); |
348 | /// }, variant: variants); |
349 | /// } |
350 | /// ``` |
351 | /// {@end-tool} |
352 | class ValueVariant<T> extends TestVariant<T> { |
353 | /// Creates a [ValueVariant] that tests the given [values]. |
354 | ValueVariant(this.values); |
355 | |
356 | /// Returns the value currently under test. |
357 | T? get currentValue => _currentValue; |
358 | T? _currentValue; |
359 | |
360 | @override |
361 | final Set<T> values; |
362 | |
363 | @override |
364 | String describeValue(T value) => value.toString().replaceFirst(' $T.' , '' ); |
365 | |
366 | @override |
367 | Future<T> setUp(T value) async => _currentValue = value; |
368 | |
369 | @override |
370 | Future<void> tearDown(T value, T memento) async {} |
371 | } |
372 | |
373 | /// The warning message to show when a benchmark is performed with assert on. |
374 | const String kDebugWarning = ''' |
375 | ┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓ |
376 | ┇ ⚠ THIS BENCHMARK IS BEING RUN IN DEBUG MODE ⚠ ┇ |
377 | ┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦ |
378 | │ │ |
379 | │ Numbers obtained from a benchmark while asserts are │ |
380 | │ enabled will not accurately reflect the performance │ |
381 | │ that will be experienced by end users using release ╎ |
382 | │ builds. Benchmarks should be run using this command ╎ |
383 | │ line: "flutter run --profile test.dart" or ┊ |
384 | │ or "flutter drive --profile -t test.dart". ┊ |
385 | │ ┊ |
386 | └─────────────────────────────────────────────────╌┄┈ 🐢 |
387 | ''' ; |
388 | |
389 | /// Runs the [callback] inside the Flutter benchmark environment. |
390 | /// |
391 | /// Use this function for benchmarking custom [StatelessWidget]s and |
392 | /// [StatefulWidget]s when you want to be able to use features from |
393 | /// [TestWidgetsFlutterBinding]. The callback, when run, will be given |
394 | /// a new instance of [WidgetTester]. The [find] object provides |
395 | /// convenient widget [Finder]s for use with the [WidgetTester]. |
396 | /// |
397 | /// The callback can be asynchronous (using `async`/`await` or using |
398 | /// explicit [Future]s). If it is, then [benchmarkWidgets] will return |
399 | /// a [Future] that completes when the callback's does. Otherwise, it |
400 | /// will return a Future that is always complete. |
401 | /// |
402 | /// If the callback is asynchronous, make sure you `await` the call |
403 | /// to [benchmarkWidgets], otherwise it won't run! |
404 | /// |
405 | /// If the `semanticsEnabled` parameter is set to `true`, |
406 | /// [WidgetTester.ensureSemantics] will have been called before the tester is |
407 | /// passed to the `callback`, and that handle will automatically be disposed |
408 | /// after the callback is finished. |
409 | /// |
410 | /// Benchmarks must not be run in debug mode, because the performance is not |
411 | /// representative. To avoid this, this function will print a big message if it |
412 | /// is run in debug mode. Unit tests of this method pass `mayRunWithAsserts`, |
413 | /// but it should not be used for actual benchmarking. |
414 | /// |
415 | /// Example: |
416 | /// |
417 | /// main() async { |
418 | /// assert(false); // fail in debug mode |
419 | /// await benchmarkWidgets((WidgetTester tester) async { |
420 | /// await tester.pumpWidget(MyWidget()); |
421 | /// final Stopwatch timer = Stopwatch()..start(); |
422 | /// for (int index = 0; index < 10000; index += 1) { |
423 | /// await tester.tap(find.text('Tap me')); |
424 | /// await tester.pump(); |
425 | /// } |
426 | /// timer.stop(); |
427 | /// debugPrint('Time taken: ${timer.elapsedMilliseconds}ms'); |
428 | /// }); |
429 | /// exit(0); |
430 | /// } |
431 | Future<void> benchmarkWidgets( |
432 | WidgetTesterCallback callback, { |
433 | bool mayRunWithAsserts = false, |
434 | bool semanticsEnabled = false, |
435 | }) { |
436 | assert(() { |
437 | if (mayRunWithAsserts) { |
438 | return true; |
439 | } |
440 | debugPrint(kDebugWarning); |
441 | return true; |
442 | }()); |
443 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
444 | assert(binding is! AutomatedTestWidgetsFlutterBinding); |
445 | final WidgetTester tester = WidgetTester._(binding); |
446 | SemanticsHandle? semanticsHandle; |
447 | if (semanticsEnabled) { |
448 | semanticsHandle = tester.ensureSemantics(); |
449 | } |
450 | tester._recordNumberOfSemanticsHandles(); |
451 | return binding.runTest(() async { |
452 | await callback(tester); |
453 | semanticsHandle?.dispose(); |
454 | }, tester._endOfTestVerifications); |
455 | } |
456 | |
457 | /// Assert that `actual` matches `matcher`. |
458 | /// |
459 | /// See [matcher_expect.expect] for details. This is a variant of that function |
460 | /// that additionally verifies that there are no asynchronous APIs |
461 | /// that have not yet resolved. |
462 | /// |
463 | /// See also: |
464 | /// |
465 | /// * [expectLater] for use with asynchronous matchers. |
466 | void expect( |
467 | dynamic actual, |
468 | dynamic matcher, { |
469 | String? reason, |
470 | dynamic skip, // true or a String |
471 | }) { |
472 | TestAsyncUtils.guardSync(); |
473 | matcher_expect.expect(actual, matcher, reason: reason, skip: skip); |
474 | } |
475 | |
476 | /// Assert that `actual` matches `matcher`. |
477 | /// |
478 | /// See [matcher_expect.expect] for details. This variant will _not_ check that |
479 | /// there are no outstanding asynchronous API requests. As such, it can be |
480 | /// called from, e.g., callbacks that are run during build or layout, or in the |
481 | /// completion handlers of futures that execute in response to user input. |
482 | /// |
483 | /// Generally, it is better to use [expect], which does include checks to ensure |
484 | /// that asynchronous APIs are not being called. |
485 | void expectSync(dynamic actual, dynamic matcher, {String? reason}) { |
486 | matcher_expect.expect(actual, matcher, reason: reason); |
487 | } |
488 | |
489 | /// Just like [expect], but returns a [Future] that completes when the matcher |
490 | /// has finished matching. |
491 | /// |
492 | /// See [matcher_expect.expectLater] for details. |
493 | /// |
494 | /// If the matcher fails asynchronously, that failure is piped to the returned |
495 | /// future where it can be handled by user code. If it is not handled by user |
496 | /// code, the test will fail. |
497 | Future<void> expectLater( |
498 | dynamic actual, |
499 | dynamic matcher, { |
500 | String? reason, |
501 | dynamic skip, // true or a String |
502 | }) { |
503 | // We can't wrap the delegate in a guard, or we'll hit async barriers in |
504 | // [TestWidgetsFlutterBinding] while we're waiting for the matcher to complete |
505 | TestAsyncUtils.guardSync(); |
506 | return matcher_expect |
507 | .expectLater(actual, matcher, reason: reason, skip: skip) |
508 | .then<void>((dynamic value) => null); |
509 | } |
510 | |
511 | /// Class that programmatically interacts with widgets and the test environment. |
512 | /// |
513 | /// Typically, a test uses [pumpWidget] to load a widget tree (in a manner very |
514 | /// similar to how [runApp] works in a Flutter application). Then, methods such |
515 | /// as [tap], [drag], [enterText], [fling], [longPress], etc, can be used to |
516 | /// interact with the application. The application runs in a [FakeAsync] zone, |
517 | /// which allows time to be stepped forward deliberately; this is done using the |
518 | /// [pump] method. |
519 | /// |
520 | /// The [expect] function can then be used to examine the state of the |
521 | /// application, typically using [Finder]s such as those in the [find] |
522 | /// namespace, and [Matcher]s such as [findsOneWidget]. |
523 | /// |
524 | /// ```dart |
525 | /// testWidgets('MyWidget', (WidgetTester tester) async { |
526 | /// await tester.pumpWidget(const MyWidget()); |
527 | /// await tester.tap(find.text('Save')); |
528 | /// await tester.pump(); // allow the application to handle |
529 | /// await tester.pump(const Duration(seconds: 1)); // skip past the animation |
530 | /// expect(find.text('Success'), findsOneWidget); |
531 | /// }); |
532 | /// ``` |
533 | /// |
534 | /// For convenience, instances of this class (such as the one provided by |
535 | /// `testWidgets`) can be used as the `vsync` for `AnimationController` objects. |
536 | /// |
537 | /// When the binding is [LiveTestWidgetsFlutterBinding], events from |
538 | /// [LiveTestWidgetsFlutterBinding.deviceEventDispatcher] will be handled in |
539 | /// [dispatchEvent]. Thus, using `flutter run` to run a test lets one tap on |
540 | /// the screen to generate [Finder]s relevant to the test. |
541 | class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider { |
542 | WidgetTester._(super.binding) { |
543 | if (binding is LiveTestWidgetsFlutterBinding) { |
544 | (binding as LiveTestWidgetsFlutterBinding).deviceEventDispatcher = this; |
545 | } |
546 | } |
547 | |
548 | /// The description string of the test currently being run. |
549 | String get testDescription => _testDescription; |
550 | String _testDescription = '' ; |
551 | |
552 | /// The binding instance used by the testing framework. |
553 | @override |
554 | TestWidgetsFlutterBinding get binding => super.binding as TestWidgetsFlutterBinding; |
555 | |
556 | /// Renders the UI from the given [widget]. |
557 | /// |
558 | /// Calls [runApp] with the given widget, then triggers a frame and flushes |
559 | /// microtasks, by calling [pump] with the same `duration` (if any). The |
560 | /// supplied [EnginePhase] is the final phase reached during the pump pass; if |
561 | /// not supplied, the whole pass is executed. |
562 | /// |
563 | /// Subsequent calls to this is different from [pump] in that it forces a full |
564 | /// rebuild of the tree, even if [widget] is the same as the previous call. |
565 | /// [pump] will only rebuild the widgets that have changed. |
566 | /// |
567 | /// This method should not be used as the first parameter to an [expect] or |
568 | /// [expectLater] call to test that a widget throws an exception. Instead, use |
569 | /// [TestWidgetsFlutterBinding.takeException]. |
570 | /// |
571 | /// {@tool snippet} |
572 | /// ```dart |
573 | /// testWidgets('MyWidget asserts invalid bounds', (WidgetTester tester) async { |
574 | /// await tester.pumpWidget(const MyWidget()); |
575 | /// expect(tester.takeException(), isAssertionError); // or isNull, as appropriate. |
576 | /// }); |
577 | /// ``` |
578 | /// {@end-tool} |
579 | /// |
580 | /// By default, the provided `widget` is rendered into [WidgetTester.view], |
581 | /// whose properties tests can modify to simulate different scenarios (e.g. |
582 | /// running on a large/small screen). Tests that want to control the |
583 | /// [FlutterView] into which content is rendered can set `wrapWithView` to |
584 | /// false and use [View] widgets in the provided `widget` tree to specify the |
585 | /// desired [FlutterView]s. |
586 | /// |
587 | /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how |
588 | /// this method works when the test is run with `flutter run`. |
589 | Future<void> pumpWidget( |
590 | Widget widget, { |
591 | Duration? duration, |
592 | EnginePhase phase = EnginePhase.sendSemanticsUpdate, |
593 | bool wrapWithView = true, |
594 | }) { |
595 | return TestAsyncUtils.guard<void>(() { |
596 | binding.attachRootWidget(wrapWithView ? binding.wrapWithDefaultView(widget) : widget); |
597 | binding.scheduleFrame(); |
598 | return binding.pump(duration, phase); |
599 | }); |
600 | } |
601 | |
602 | @override |
603 | Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) { |
604 | assert(records.isNotEmpty); |
605 | return TestAsyncUtils.guard<List<Duration>>(() async { |
606 | final List<Duration> handleTimeStampDiff = <Duration>[]; |
607 | DateTime? startTime; |
608 | for (final PointerEventRecord record in records) { |
609 | final DateTime now = binding.clock.now(); |
610 | startTime ??= now; |
611 | // So that the first event is promised to receive a zero timeDiff |
612 | final Duration timeDiff = record.timeDelay - now.difference(startTime); |
613 | if (timeDiff.isNegative) { |
614 | // Flush all past events |
615 | handleTimeStampDiff.add(-timeDiff); |
616 | for (final PointerEvent event in record.events) { |
617 | binding.handlePointerEventForSource(event, source: TestBindingEventSource.test); |
618 | } |
619 | } else { |
620 | await binding.pump(); |
621 | await binding.delayed(timeDiff); |
622 | handleTimeStampDiff.add(binding.clock.now().difference(startTime) - record.timeDelay); |
623 | for (final PointerEvent event in record.events) { |
624 | binding.handlePointerEventForSource(event, source: TestBindingEventSource.test); |
625 | } |
626 | } |
627 | } |
628 | await binding.pump(); |
629 | // This makes sure that a gesture is completed, with no more pointers |
630 | // active. |
631 | return handleTimeStampDiff; |
632 | }); |
633 | } |
634 | |
635 | /// Triggers a frame after `duration` amount of time. |
636 | /// |
637 | /// This makes the framework act as if the application had janked (missed |
638 | /// frames) for `duration` amount of time, and then received a "Vsync" signal |
639 | /// to paint the application. |
640 | /// |
641 | /// For a [FakeAsync] environment (typically in `flutter test`), this advances |
642 | /// time and timeout counting; for a live environment this delays `duration` |
643 | /// time. |
644 | /// |
645 | /// This is a convenience function that just calls |
646 | /// [TestWidgetsFlutterBinding.pump]. |
647 | /// |
648 | /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how |
649 | /// this method works when the test is run with `flutter run`. |
650 | @override |
651 | Future<void> pump([Duration? duration, EnginePhase phase = EnginePhase.sendSemanticsUpdate]) { |
652 | return TestAsyncUtils.guard<void>(() => binding.pump(duration, phase)); |
653 | } |
654 | |
655 | /// Triggers a frame after `duration` amount of time, return as soon as the frame is drawn. |
656 | /// |
657 | /// This enables driving an artificially high CPU load by rendering frames in |
658 | /// a tight loop. It must be used with the frame policy set to |
659 | /// [LiveTestWidgetsFlutterBindingFramePolicy.benchmark]. |
660 | /// |
661 | /// Similarly to [pump], this doesn't actually wait for `duration`, just |
662 | /// advances the clock. |
663 | Future<void> pumpBenchmark(Duration duration) async { |
664 | assert(() { |
665 | final TestWidgetsFlutterBinding widgetsBinding = binding; |
666 | return widgetsBinding is LiveTestWidgetsFlutterBinding && |
667 | widgetsBinding.framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark; |
668 | }()); |
669 | |
670 | dynamic caughtException; |
671 | StackTrace? stackTrace; |
672 | void handleError(dynamic error, StackTrace trace) { |
673 | caughtException ??= error; |
674 | stackTrace ??= trace; |
675 | } |
676 | |
677 | await Future<void>.microtask(() { |
678 | binding.handleBeginFrame(duration); |
679 | }).catchError(handleError); |
680 | await idle(); |
681 | await Future<void>.microtask(() { |
682 | binding.handleDrawFrame(); |
683 | }).catchError(handleError); |
684 | await idle(); |
685 | |
686 | if (caughtException != null) { |
687 | Error.throwWithStackTrace(caughtException as Object, stackTrace!); |
688 | } |
689 | } |
690 | |
691 | @override |
692 | Future<int> pumpAndSettle([ |
693 | Duration duration = const Duration(milliseconds: 100), |
694 | EnginePhase phase = EnginePhase.sendSemanticsUpdate, |
695 | Duration timeout = const Duration(minutes: 10), |
696 | ]) { |
697 | assert(duration > Duration.zero); |
698 | assert(timeout > Duration.zero); |
699 | assert(() { |
700 | final WidgetsBinding binding = this.binding; |
701 | if (binding is LiveTestWidgetsFlutterBinding && |
702 | binding.framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark) { |
703 | matcher_expect.fail( |
704 | 'When using LiveTestWidgetsFlutterBindingFramePolicy.benchmark, ' |
705 | 'hasScheduledFrame is never set to true. This means that pumpAndSettle() ' |
706 | 'cannot be used, because it has no way to know if the application has ' |
707 | 'stopped registering new frames.' , |
708 | ); |
709 | } |
710 | return true; |
711 | }()); |
712 | return TestAsyncUtils.guard<int>(() async { |
713 | final DateTime endTime = binding.clock.fromNowBy(timeout); |
714 | int count = 0; |
715 | do { |
716 | if (binding.clock.now().isAfter(endTime)) { |
717 | throw FlutterError('pumpAndSettle timed out' ); |
718 | } |
719 | await binding.pump(duration, phase); |
720 | count += 1; |
721 | } while (binding.hasScheduledFrame); |
722 | return count; |
723 | }); |
724 | } |
725 | |
726 | /// Repeatedly pump frames that render the `target` widget with a fixed time |
727 | /// `interval` as many as `maxDuration` allows. |
728 | /// |
729 | /// The `maxDuration` argument is required. The `interval` argument defaults to |
730 | /// 16.683 milliseconds (59.94 FPS). |
731 | Future<void> pumpFrames( |
732 | Widget target, |
733 | Duration maxDuration, [ |
734 | Duration interval = const Duration(milliseconds: 16, microseconds: 683), |
735 | ]) { |
736 | // The interval following the last frame doesn't have to be within the fullDuration. |
737 | Duration elapsed = Duration.zero; |
738 | return TestAsyncUtils.guard<void>(() async { |
739 | binding.attachRootWidget(binding.wrapWithDefaultView(target)); |
740 | binding.scheduleFrame(); |
741 | while (elapsed < maxDuration) { |
742 | await binding.pump(interval); |
743 | elapsed += interval; |
744 | } |
745 | }); |
746 | } |
747 | |
748 | /// Simulates restoring the state of the widget tree after the application |
749 | /// is restarted. |
750 | /// |
751 | /// The method grabs the current serialized restoration data from the |
752 | /// [RestorationManager], takes down the widget tree to destroy all in-memory |
753 | /// state, and then restores the widget tree from the serialized restoration |
754 | /// data. |
755 | Future<void> restartAndRestore() async { |
756 | assert( |
757 | binding.restorationManager.debugRootBucketAccessed, |
758 | 'The current widget tree did not inject the root bucket of the RestorationManager and ' |
759 | 'therefore no restoration data has been collected to restore from. Did you forget to wrap ' |
760 | 'your widget tree in a RootRestorationScope?' , |
761 | ); |
762 | return TestAsyncUtils.guard<void>(() async { |
763 | final RootWidget widget = binding.rootElement!.widget as RootWidget; |
764 | final TestRestorationData restorationData = binding.restorationManager.restorationData; |
765 | runApp(Container(key: UniqueKey())); |
766 | await pump(); |
767 | binding.restorationManager.restoreFrom(restorationData); |
768 | binding.attachToBuildOwner(widget); |
769 | binding.scheduleFrame(); |
770 | return binding.pump(); |
771 | }); |
772 | } |
773 | |
774 | /// Retrieves the current restoration data from the [RestorationManager]. |
775 | /// |
776 | /// The returned [TestRestorationData] describes the current state of the |
777 | /// widget tree under test and can be provided to [restoreFrom] to restore |
778 | /// the widget tree to the state described by this data. |
779 | Future<TestRestorationData> getRestorationData() async { |
780 | assert( |
781 | binding.restorationManager.debugRootBucketAccessed, |
782 | 'The current widget tree did not inject the root bucket of the RestorationManager and ' |
783 | 'therefore no restoration data has been collected. Did you forget to wrap your widget tree ' |
784 | 'in a RootRestorationScope?' , |
785 | ); |
786 | return binding.restorationManager.restorationData; |
787 | } |
788 | |
789 | /// Restores the widget tree under test to the state described by the |
790 | /// provided [TestRestorationData]. |
791 | /// |
792 | /// The data provided to this method is usually obtained from |
793 | /// [getRestorationData]. |
794 | Future<void> restoreFrom(TestRestorationData data) { |
795 | binding.restorationManager.restoreFrom(data); |
796 | return pump(); |
797 | } |
798 | |
799 | /// Runs a [callback] that performs real asynchronous work. |
800 | /// |
801 | /// This is intended for callers who need to call asynchronous methods where |
802 | /// the methods spawn isolates or OS threads and thus cannot be executed |
803 | /// synchronously by calling [pump]. |
804 | /// |
805 | /// If callers were to run these types of asynchronous tasks directly in |
806 | /// their test methods, they run the possibility of encountering deadlocks. |
807 | /// |
808 | /// If [callback] completes successfully, this will return the future |
809 | /// returned by [callback]. |
810 | /// |
811 | /// If [callback] completes with an error, the error will be caught by the |
812 | /// Flutter framework and made available via [takeException], and this method |
813 | /// will return a future that completes with `null`. |
814 | /// |
815 | /// Re-entrant calls to this method are not allowed; callers of this method |
816 | /// are required to wait for the returned future to complete before calling |
817 | /// this method again. Attempts to do otherwise will result in a |
818 | /// [TestFailure] error being thrown. |
819 | /// |
820 | /// If your widget test hangs and you are using [runAsync], chances are your |
821 | /// code depends on the result of a task that did not complete. Fake async |
822 | /// environment is unable to resolve a future that was created in [runAsync]. |
823 | /// If you observe such behavior or flakiness, you have a number of options: |
824 | /// |
825 | /// * Consider restructuring your code so you do not need [runAsync]. This is |
826 | /// the optimal solution as widget tests are designed to run in fake async |
827 | /// environment. |
828 | /// |
829 | /// * Expose a [Future] in your application code that signals the readiness of |
830 | /// your widget tree, then await that future inside [callback]. |
831 | Future<T?> runAsync<T>( |
832 | Future<T> Function() callback, { |
833 | @Deprecated( |
834 | 'This is no longer supported and has no effect. ' |
835 | 'This feature was deprecated after v3.12.0-1.1.pre.' , |
836 | ) |
837 | Duration additionalTime = const Duration(milliseconds: 1000), |
838 | }) => binding.runAsync<T?>(callback); |
839 | |
840 | /// Whether there are any transient callbacks scheduled. |
841 | /// |
842 | /// This essentially checks whether all animations have completed. |
843 | /// |
844 | /// See also: |
845 | /// |
846 | /// * [pumpAndSettle], which essentially calls [pump] until there are no |
847 | /// scheduled frames. |
848 | /// * [SchedulerBinding.transientCallbackCount], which is the value on which |
849 | /// this is based. |
850 | /// * [SchedulerBinding.hasScheduledFrame], which is true whenever a frame is |
851 | /// pending. [SchedulerBinding.hasScheduledFrame] is made true when a |
852 | /// widget calls [State.setState], even if there are no transient callbacks |
853 | /// scheduled. This is what [pumpAndSettle] uses. |
854 | bool get hasRunningAnimations => binding.transientCallbackCount > 0; |
855 | |
856 | @override |
857 | HitTestResult hitTestOnBinding(Offset location, {int? viewId}) { |
858 | viewId ??= view.viewId; |
859 | final RenderView renderView = binding.renderViews.firstWhere( |
860 | (RenderView r) => r.flutterView.viewId == viewId, |
861 | ); |
862 | location = binding.localToGlobal(location, renderView); |
863 | return super.hitTestOnBinding(location, viewId: viewId); |
864 | } |
865 | |
866 | @override |
867 | Future<void> sendEventToBinding(PointerEvent event) { |
868 | return TestAsyncUtils.guard<void>(() async { |
869 | binding.handlePointerEventForSource(event, source: TestBindingEventSource.test); |
870 | }); |
871 | } |
872 | |
873 | /// Handler for device events caught by the binding in live test mode. |
874 | /// |
875 | /// [PointerDownEvent]s received here will only print a diagnostic message |
876 | /// showing possible [Finder]s that can be used to interact with the widget at |
877 | /// the location of [result]. |
878 | @override |
879 | void dispatchEvent(PointerEvent event, HitTestResult result) { |
880 | if (event is PointerDownEvent) { |
881 | final RenderObject innerTarget = result.path |
882 | .map((HitTestEntry candidate) => candidate.target) |
883 | .whereType<RenderObject>() |
884 | .first; |
885 | final Element? innerTargetElement = binding.renderViews.contains(innerTarget) |
886 | ? null |
887 | : _lastWhereOrNull( |
888 | collectAllElementsFrom(binding.rootElement!, skipOffstage: true), |
889 | (Element element) => element.renderObject == innerTarget, |
890 | ); |
891 | if (innerTargetElement == null) { |
892 | printToConsole('No widgets found at ${event.position}.' ); |
893 | return; |
894 | } |
895 | final List<Element> candidates = <Element>[]; |
896 | innerTargetElement.visitAncestorElements((Element element) { |
897 | candidates.add(element); |
898 | return true; |
899 | }); |
900 | assert(candidates.isNotEmpty); |
901 | String? descendantText; |
902 | int numberOfWithTexts = 0; |
903 | int numberOfTypes = 0; |
904 | int totalNumber = 0; |
905 | printToConsole('Some possible finders for the widgets at ${event.position}:' ); |
906 | for (final Element element in candidates) { |
907 | if (totalNumber > 13) { |
908 | break; |
909 | } |
910 | totalNumber += 1; // optimistically assume we'll be able to describe it |
911 | |
912 | final Widget widget = element.widget; |
913 | if (widget is Tooltip) { |
914 | final String message = widget.message ?? widget.richMessage!.toPlainText(); |
915 | final Iterable<Element> matches = find.byTooltip(message).evaluate(); |
916 | if (matches.length == 1) { |
917 | printToConsole(" find.byTooltip(' $message')" ); |
918 | continue; |
919 | } |
920 | } |
921 | |
922 | if (widget is Text) { |
923 | assert(descendantText == null); |
924 | assert(widget.data != null || widget.textSpan != null); |
925 | final String text = widget.data ?? widget.textSpan!.toPlainText(); |
926 | final Iterable<Element> matches = find.text(text).evaluate(); |
927 | descendantText = widget.data; |
928 | if (matches.length == 1) { |
929 | printToConsole(" find.text(' $text')" ); |
930 | continue; |
931 | } |
932 | } |
933 | |
934 | final Key? key = widget.key; |
935 | if (key is ValueKey<dynamic>) { |
936 | final String? keyLabel = switch (key.value) { |
937 | int() || double() || bool() => 'const ${key.runtimeType}( ${key.value})' , |
938 | final String value => "const Key(' $value')" , |
939 | _ => null, |
940 | }; |
941 | if (keyLabel != null) { |
942 | final Iterable<Element> matches = find.byKey(key).evaluate(); |
943 | if (matches.length == 1) { |
944 | printToConsole(' find.byKey( $keyLabel)' ); |
945 | continue; |
946 | } |
947 | } |
948 | } |
949 | |
950 | if (!_isPrivate(widget.runtimeType)) { |
951 | if (numberOfTypes < 5) { |
952 | final Iterable<Element> matches = find.byType(widget.runtimeType).evaluate(); |
953 | if (matches.length == 1) { |
954 | printToConsole(' find.byType( ${widget.runtimeType})' ); |
955 | numberOfTypes += 1; |
956 | continue; |
957 | } |
958 | } |
959 | |
960 | if (descendantText != null && numberOfWithTexts < 5) { |
961 | final Iterable<Element> matches = find |
962 | .widgetWithText(widget.runtimeType, descendantText) |
963 | .evaluate(); |
964 | if (matches.length == 1) { |
965 | printToConsole(" find.widgetWithText( ${widget.runtimeType}, ' $descendantText')" ); |
966 | numberOfWithTexts += 1; |
967 | continue; |
968 | } |
969 | } |
970 | } |
971 | |
972 | if (!_isPrivate(element.runtimeType)) { |
973 | final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate(); |
974 | if (matches.length == 1) { |
975 | printToConsole(' find.byElementType( ${element.runtimeType})' ); |
976 | continue; |
977 | } |
978 | } |
979 | |
980 | totalNumber -= 1; // if we got here, we didn't actually find something to say about it |
981 | } |
982 | if (totalNumber == 0) { |
983 | printToConsole(' <could not come up with any unique finders>' ); |
984 | } |
985 | } |
986 | } |
987 | |
988 | bool _isPrivate(Type type) { |
989 | // used above so that we don't suggest matchers for private types |
990 | return '_' .matchAsPrefix(type.toString()) != null; |
991 | } |
992 | |
993 | /// Returns the exception most recently caught by the Flutter framework. |
994 | /// |
995 | /// See [TestWidgetsFlutterBinding.takeException] for details. |
996 | dynamic takeException() { |
997 | return binding.takeException(); |
998 | } |
999 | |
1000 | /// {@macro flutter.flutter_test.TakeAccessibilityAnnouncements} |
1001 | /// |
1002 | /// See [TestWidgetsFlutterBinding.takeAnnouncements] for details. |
1003 | List<CapturedAccessibilityAnnouncement> takeAnnouncements() { |
1004 | return binding.takeAnnouncements(); |
1005 | } |
1006 | |
1007 | /// Acts as if the application went idle. |
1008 | /// |
1009 | /// Runs all remaining microtasks, including those scheduled as a result of |
1010 | /// running them, until there are no more microtasks scheduled. Then, runs any |
1011 | /// previously scheduled timers with zero time, and completes the returned future. |
1012 | /// |
1013 | /// May result in an infinite loop or run out of memory if microtasks continue |
1014 | /// to recursively schedule new microtasks. Will not run any timers scheduled |
1015 | /// after this method was invoked, even if they are zero-time timers. |
1016 | Future<void> idle() { |
1017 | return TestAsyncUtils.guard<void>(() => binding.idle()); |
1018 | } |
1019 | |
1020 | Set<Ticker>? _tickers; |
1021 | |
1022 | @override |
1023 | Ticker createTicker(TickerCallback onTick) { |
1024 | _tickers ??= <_TestTicker>{}; |
1025 | final _TestTicker result = _TestTicker(onTick, _removeTicker); |
1026 | _tickers!.add(result); |
1027 | return result; |
1028 | } |
1029 | |
1030 | void _removeTicker(_TestTicker ticker) { |
1031 | assert(_tickers != null); |
1032 | assert(_tickers!.contains(ticker)); |
1033 | _tickers!.remove(ticker); |
1034 | } |
1035 | |
1036 | /// Throws an exception if any tickers created by the [WidgetTester] are still |
1037 | /// active when the method is called. |
1038 | /// |
1039 | /// An argument can be specified to provide a string that will be used in the |
1040 | /// error message. It should be an adverbial phrase describing the current |
1041 | /// situation, such as "at the end of the test". |
1042 | void verifyTickersWereDisposed([String when = 'when none should have been' ]) { |
1043 | if (_tickers != null) { |
1044 | for (final Ticker ticker in _tickers!) { |
1045 | if (ticker.isActive) { |
1046 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1047 | ErrorSummary('A Ticker was active $when.' ), |
1048 | ErrorDescription('All Tickers must be disposed.' ), |
1049 | ErrorHint( |
1050 | 'Tickers used by AnimationControllers ' |
1051 | 'should be disposed by calling dispose() on the AnimationController itself. ' |
1052 | 'Otherwise, the ticker will leak.' , |
1053 | ), |
1054 | ticker.describeForError('The offending ticker was' ), |
1055 | ]); |
1056 | } |
1057 | } |
1058 | } |
1059 | } |
1060 | |
1061 | void _endOfTestVerifications() { |
1062 | verifyTickersWereDisposed('at the end of the test' ); |
1063 | _verifySemanticsHandlesWereDisposed(); |
1064 | } |
1065 | |
1066 | void _verifySemanticsHandlesWereDisposed() { |
1067 | assert(_lastRecordedSemanticsHandles != null); |
1068 | // TODO(goderbauer): Fix known leak in web engine when running integration tests and remove this "correction", https://github.com/flutter/flutter/issues/121640. |
1069 | final int knownWebEngineLeakForLiveTestsCorrection = |
1070 | kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 1 : 0; |
1071 | |
1072 | if (_currentSemanticsHandles - knownWebEngineLeakForLiveTestsCorrection > |
1073 | _lastRecordedSemanticsHandles!) { |
1074 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1075 | ErrorSummary('A SemanticsHandle was active at the end of the test.' ), |
1076 | ErrorDescription( |
1077 | 'All SemanticsHandle instances must be disposed by calling dispose() on ' |
1078 | 'the SemanticsHandle.' , |
1079 | ), |
1080 | ]); |
1081 | } |
1082 | _lastRecordedSemanticsHandles = null; |
1083 | } |
1084 | |
1085 | int? _lastRecordedSemanticsHandles; |
1086 | |
1087 | // TODO(goderbauer): Only use binding.debugOutstandingSemanticsHandles when deprecated binding.pipelineOwner is removed. |
1088 | int get _currentSemanticsHandles => |
1089 | binding.debugOutstandingSemanticsHandles + |
1090 | binding.pipelineOwner.debugOutstandingSemanticsHandles; |
1091 | |
1092 | void _recordNumberOfSemanticsHandles() { |
1093 | _lastRecordedSemanticsHandles = _currentSemanticsHandles; |
1094 | } |
1095 | |
1096 | /// Returns the TestTextInput singleton. |
1097 | /// |
1098 | /// Typical app tests will not need to use this value. To add text to widgets |
1099 | /// like [TextField] or [TextFormField], call [enterText]. |
1100 | /// |
1101 | /// Some of the properties and methods on this value are only valid if the |
1102 | /// binding's [TestWidgetsFlutterBinding.registerTestTextInput] flag is set to |
1103 | /// true as a test is starting (meaning that the keyboard is to be simulated |
1104 | /// by the test framework). If those members are accessed when using a binding |
1105 | /// that sets this flag to false, they will throw. |
1106 | TestTextInput get testTextInput => binding.testTextInput; |
1107 | |
1108 | /// Give the text input widget specified by [finder] the focus, as if the |
1109 | /// onscreen keyboard had appeared. |
1110 | /// |
1111 | /// Implies a call to [pump]. |
1112 | /// |
1113 | /// The widget specified by [finder] must be an [EditableText] or have |
1114 | /// an [EditableText] descendant. For example `find.byType(TextField)` |
1115 | /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`. |
1116 | /// |
1117 | /// Tests that just need to add text to widgets like [TextField] |
1118 | /// or [TextFormField] only need to call [enterText]. |
1119 | Future<void> showKeyboard(FinderBase<Element> finder) async { |
1120 | bool skipOffstage = true; |
1121 | if (finder is Finder) { |
1122 | skipOffstage = finder.skipOffstage; |
1123 | } |
1124 | return TestAsyncUtils.guard<void>(() async { |
1125 | final EditableTextState editable = state<EditableTextState>( |
1126 | find.descendant( |
1127 | of: finder, |
1128 | matching: find.byType(EditableText, skipOffstage: skipOffstage), |
1129 | matchRoot: true, |
1130 | ), |
1131 | ); |
1132 | // Setting focusedEditable causes the binding to call requestKeyboard() |
1133 | // on the EditableTextState, which itself eventually calls TextInput.attach |
1134 | // to establish the connection. |
1135 | binding.focusedEditable = editable; |
1136 | await pump(); |
1137 | }); |
1138 | } |
1139 | |
1140 | /// Give the text input widget specified by [finder] the focus and replace its |
1141 | /// content with [text], as if it had been provided by the onscreen keyboard. |
1142 | /// |
1143 | /// The widget specified by [finder] must be an [EditableText] or have |
1144 | /// an [EditableText] descendant. For example `find.byType(TextField)` |
1145 | /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`. |
1146 | /// |
1147 | /// When the returned future completes, the text input widget's text will be |
1148 | /// exactly `text`, and the caret will be placed at the end of `text`. |
1149 | /// |
1150 | /// To just give [finder] the focus without entering any text, |
1151 | /// see [showKeyboard]. |
1152 | /// |
1153 | /// To enter text into other widgets (e.g. a custom widget that maintains a |
1154 | /// TextInputConnection the way that a [EditableText] does), first ensure that |
1155 | /// that widget has an open connection (e.g. by using [tap] to focus it), |
1156 | /// then call `testTextInput.enterText` directly (see |
1157 | /// [TestTextInput.enterText]). |
1158 | Future<void> enterText(FinderBase<Element> finder, String text) async { |
1159 | return TestAsyncUtils.guard<void>(() async { |
1160 | await showKeyboard(finder); |
1161 | testTextInput.enterText(text); |
1162 | await idle(); |
1163 | }); |
1164 | } |
1165 | |
1166 | /// Makes an effort to dismiss the current page with a Material [Scaffold] or |
1167 | /// a [CupertinoPageScaffold]. |
1168 | /// |
1169 | /// Will throw an error if there is no back button in the page. |
1170 | Future<void> pageBack() async { |
1171 | return TestAsyncUtils.guard<void>(() async { |
1172 | Finder backButton = find.byTooltip('Back' ); |
1173 | if (backButton.evaluate().isEmpty) { |
1174 | backButton = find.byType(CupertinoNavigationBarBackButton); |
1175 | } |
1176 | |
1177 | expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen' ); |
1178 | |
1179 | await tap(backButton); |
1180 | }); |
1181 | } |
1182 | |
1183 | @override |
1184 | void printToConsole(String message) { |
1185 | binding.debugPrintOverride(message); |
1186 | } |
1187 | } |
1188 | |
1189 | typedef _TickerDisposeCallback = void Function(_TestTicker ticker); |
1190 | |
1191 | class _TestTicker extends Ticker { |
1192 | _TestTicker(super.onTick, this._onDispose); |
1193 | |
1194 | final _TickerDisposeCallback _onDispose; |
1195 | |
1196 | @override |
1197 | void dispose() { |
1198 | _onDispose(this); |
1199 | super.dispose(); |
1200 | } |
1201 | } |
1202 | |