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