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