| 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 | import 'dart:async'; |
| 6 | import 'dart:io'; |
| 7 | |
| 8 | import 'package:flutter/cupertino.dart'; |
| 9 | import 'package:flutter/foundation.dart'; |
| 10 | import 'package:flutter/gestures.dart'; |
| 11 | import 'package:flutter/material.dart'; |
| 12 | import 'package:flutter/rendering.dart'; |
| 13 | import 'package:flutter/scheduler.dart'; |
| 14 | import 'package:flutter/services.dart'; |
| 15 | import 'package:flutter_test/flutter_test.dart'; |
| 16 | import 'package:matcher/expect.dart' as matcher; |
| 17 | import 'package:matcher/src/expect/async_matcher.dart' ; |
| 18 | |
| 19 | import 'multi_view_testing.dart'; |
| 20 | |
| 21 | void main() { |
| 22 | group('expectLater' , () { |
| 23 | testWidgets('completes when matcher completes' , (WidgetTester tester) async { |
| 24 | final Completer<void> completer = Completer<void>(); |
| 25 | final Future<void> future = expectLater(null, FakeMatcher(completer)); |
| 26 | String? result; |
| 27 | future.then<void>((void value) { |
| 28 | result = '123' ; |
| 29 | }); |
| 30 | matcher.expect(result, isNull); |
| 31 | completer.complete(); |
| 32 | matcher.expect(result, isNull); |
| 33 | await future; |
| 34 | await tester.pump(); |
| 35 | matcher.expect(result, '123' ); |
| 36 | }); |
| 37 | |
| 38 | testWidgets('respects the skip flag' , (WidgetTester tester) async { |
| 39 | final Completer<void> completer = Completer<void>(); |
| 40 | final Future<void> future = expectLater( |
| 41 | null, |
| 42 | FakeMatcher(completer), |
| 43 | skip: 'testing skip' , |
| 44 | ); // [intended] API testing |
| 45 | bool completed = false; |
| 46 | future.then<void>((_) { |
| 47 | completed = true; |
| 48 | }); |
| 49 | matcher.expect(completed, isFalse); |
| 50 | await future; |
| 51 | matcher.expect(completed, isTrue); |
| 52 | }); |
| 53 | }); |
| 54 | |
| 55 | group('group retry flag allows test to run multiple times' , () { |
| 56 | bool retried = false; |
| 57 | group('the group with retry flag' , () { |
| 58 | testWidgets('the test inside it' , (WidgetTester tester) async { |
| 59 | addTearDown(() => retried = true); |
| 60 | if (!retried) { |
| 61 | debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED' ); |
| 62 | } |
| 63 | expect(retried, isTrue); |
| 64 | }); |
| 65 | }, retry: 1); |
| 66 | }); |
| 67 | |
| 68 | group('testWidget retry flag allows test to run multiple times' , () { |
| 69 | bool retried = false; |
| 70 | testWidgets('the test with retry flag' , (WidgetTester tester) async { |
| 71 | addTearDown(() => retried = true); |
| 72 | if (!retried) { |
| 73 | debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED' ); |
| 74 | } |
| 75 | expect(retried, isTrue); |
| 76 | }, retry: 1); |
| 77 | }); |
| 78 | |
| 79 | group('respects the group skip flag' , () { |
| 80 | testWidgets('should be skipped' , (WidgetTester tester) async { |
| 81 | expect(false, true); |
| 82 | }); |
| 83 | }, skip: true); // [intended] API testing |
| 84 | |
| 85 | group('pumping' , () { |
| 86 | testWidgets('pumping' , (WidgetTester tester) async { |
| 87 | await tester.pumpWidget(const Text('foo' , textDirection: TextDirection.ltr)); |
| 88 | int count; |
| 89 | |
| 90 | final AnimationController test = AnimationController( |
| 91 | duration: const Duration(milliseconds: 5100), |
| 92 | vsync: tester, |
| 93 | ); |
| 94 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
| 95 | expect(count, 1); // it always pumps at least one frame |
| 96 | |
| 97 | test.forward(from: 0.0); |
| 98 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
| 99 | // 1 frame at t=0, starting the animation |
| 100 | // 1 frame at t=1 |
| 101 | // 1 frame at t=2 |
| 102 | // 1 frame at t=3 |
| 103 | // 1 frame at t=4 |
| 104 | // 1 frame at t=5 |
| 105 | // 1 frame at t=6, ending the animation |
| 106 | expect(count, 7); |
| 107 | |
| 108 | test.forward(from: 0.0); |
| 109 | await tester.pump(); // starts the animation |
| 110 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
| 111 | expect(count, 6); |
| 112 | |
| 113 | test.forward(from: 0.0); |
| 114 | await tester.pump(); // starts the animation |
| 115 | await tester.pump(); // has no effect |
| 116 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
| 117 | expect(count, 6); |
| 118 | }); |
| 119 | |
| 120 | testWidgets('pumpFrames' , (WidgetTester tester) async { |
| 121 | final List<int> logPaints = <int>[]; |
| 122 | int? initial; |
| 123 | |
| 124 | final Widget target = _AlwaysAnimating( |
| 125 | onPaint: () { |
| 126 | final int current = SchedulerBinding.instance.currentFrameTimeStamp.inMicroseconds; |
| 127 | initial ??= current; |
| 128 | logPaints.add(current - initial!); |
| 129 | }, |
| 130 | ); |
| 131 | |
| 132 | await tester.pumpFrames(target, const Duration(milliseconds: 55)); |
| 133 | |
| 134 | // `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump, |
| 135 | // so we expect 4 pumps of 16683 microseconds each in the 55ms duration. |
| 136 | expect(logPaints, <int>[0, 16683, 33366, 50049]); |
| 137 | logPaints.clear(); |
| 138 | |
| 139 | await tester.pumpFrames( |
| 140 | target, |
| 141 | const Duration(milliseconds: 30), |
| 142 | const Duration(milliseconds: 10), |
| 143 | ); |
| 144 | |
| 145 | // Since `pumpFrames` was given a 10ms interval per pump, we expect the |
| 146 | // results to continue from 50049 with 10000 microseconds per pump over |
| 147 | // the 30ms duration. |
| 148 | expect(logPaints, <int>[60049, 70049, 80049]); |
| 149 | }); |
| 150 | }); |
| 151 | group('pageBack' , () { |
| 152 | testWidgets('fails when there are no back buttons' , (WidgetTester tester) async { |
| 153 | await tester.pumpWidget(Container()); |
| 154 | |
| 155 | expect(expectAsync0(tester.pageBack), throwsA(isA<TestFailure>())); |
| 156 | }); |
| 157 | |
| 158 | testWidgets('successfully taps material back buttons' , (WidgetTester tester) async { |
| 159 | final TransitionDurationObserver observer = TransitionDurationObserver(); |
| 160 | await tester.pumpWidget( |
| 161 | MaterialApp( |
| 162 | navigatorObservers: <NavigatorObserver>[observer], |
| 163 | home: Center( |
| 164 | child: Builder( |
| 165 | builder: (BuildContext context) { |
| 166 | return ElevatedButton( |
| 167 | child: const Text('Next' ), |
| 168 | onPressed: () { |
| 169 | Navigator.push<void>( |
| 170 | context, |
| 171 | MaterialPageRoute<void>( |
| 172 | builder: (BuildContext context) { |
| 173 | return Scaffold(appBar: AppBar(title: const Text('Page 2' ))); |
| 174 | }, |
| 175 | ), |
| 176 | ); |
| 177 | }, |
| 178 | ); |
| 179 | }, |
| 180 | ), |
| 181 | ), |
| 182 | ), |
| 183 | ); |
| 184 | |
| 185 | await tester.tap(find.text('Next' )); |
| 186 | await tester.pump(); |
| 187 | await tester.pump(observer.transitionDuration + const Duration(milliseconds: 1)); |
| 188 | |
| 189 | expect(find.text('Next' ), findsNothing); |
| 190 | expect(find.text('Page 2' ), findsOneWidget); |
| 191 | |
| 192 | await tester.pageBack(); |
| 193 | await tester.pump(); |
| 194 | await tester.pump(observer.transitionDuration + const Duration(milliseconds: 1)); |
| 195 | |
| 196 | expect(find.text('Next' ), findsOneWidget); |
| 197 | expect(find.text('Page 2' ), findsNothing); |
| 198 | }); |
| 199 | |
| 200 | testWidgets('successfully taps cupertino back buttons' , (WidgetTester tester) async { |
| 201 | final TransitionDurationObserver observer = TransitionDurationObserver(); |
| 202 | await tester.pumpWidget( |
| 203 | MaterialApp( |
| 204 | navigatorObservers: <NavigatorObserver>[observer], |
| 205 | home: Center( |
| 206 | child: Builder( |
| 207 | builder: (BuildContext context) { |
| 208 | return CupertinoButton( |
| 209 | child: const Text('Next' ), |
| 210 | onPressed: () { |
| 211 | Navigator.push<void>( |
| 212 | context, |
| 213 | CupertinoPageRoute<void>( |
| 214 | builder: (BuildContext context) { |
| 215 | return CupertinoPageScaffold( |
| 216 | navigationBar: const CupertinoNavigationBar(middle: Text('Page 2' )), |
| 217 | child: Container(), |
| 218 | ); |
| 219 | }, |
| 220 | ), |
| 221 | ); |
| 222 | }, |
| 223 | ); |
| 224 | }, |
| 225 | ), |
| 226 | ), |
| 227 | ), |
| 228 | ); |
| 229 | |
| 230 | await tester.tap(find.text('Next' )); |
| 231 | await tester.pump(); |
| 232 | await tester.pump(observer.transitionDuration); |
| 233 | |
| 234 | await tester.pageBack(); |
| 235 | await tester.pump(); |
| 236 | await tester.pumpAndSettle(); |
| 237 | |
| 238 | expect(find.text('Next' ), findsOneWidget); |
| 239 | expect(find.text('Page 2' ), findsNothing); |
| 240 | }); |
| 241 | }); |
| 242 | |
| 243 | testWidgets('hasRunningAnimations control test' , (WidgetTester tester) async { |
| 244 | final AnimationController controller = AnimationController( |
| 245 | duration: const Duration(seconds: 1), |
| 246 | vsync: const TestVSync(), |
| 247 | ); |
| 248 | expect(tester.hasRunningAnimations, isFalse); |
| 249 | controller.forward(); |
| 250 | expect(tester.hasRunningAnimations, isTrue); |
| 251 | controller.stop(); |
| 252 | expect(tester.hasRunningAnimations, isFalse); |
| 253 | controller.forward(); |
| 254 | expect(tester.hasRunningAnimations, isTrue); |
| 255 | await tester.pumpAndSettle(); |
| 256 | expect(tester.hasRunningAnimations, isFalse); |
| 257 | }); |
| 258 | |
| 259 | testWidgets('pumpAndSettle control test' , (WidgetTester tester) async { |
| 260 | final AnimationController controller = AnimationController( |
| 261 | duration: const Duration(minutes: 525600), |
| 262 | vsync: const TestVSync(), |
| 263 | ); |
| 264 | expect(await tester.pumpAndSettle(), 1); |
| 265 | controller.forward(); |
| 266 | try { |
| 267 | await tester.pumpAndSettle(); |
| 268 | expect(true, isFalse); |
| 269 | } catch (e) { |
| 270 | expect(e, isFlutterError); |
| 271 | } |
| 272 | controller.stop(); |
| 273 | expect(await tester.pumpAndSettle(), 1); |
| 274 | controller.duration = const Duration(seconds: 1); |
| 275 | controller.forward(); |
| 276 | expect( |
| 277 | await tester.pumpAndSettle(const Duration(milliseconds: 300)), |
| 278 | 5, |
| 279 | ); // 0, 300, 600, 900, 1200ms |
| 280 | }); |
| 281 | |
| 282 | testWidgets('Input event array' , (WidgetTester tester) async { |
| 283 | final List<String> logs = <String>[]; |
| 284 | |
| 285 | await tester.pumpWidget( |
| 286 | Directionality( |
| 287 | textDirection: TextDirection.ltr, |
| 288 | child: Listener( |
| 289 | onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}' ), |
| 290 | onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}' ), |
| 291 | onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}' ), |
| 292 | child: const Text('test' ), |
| 293 | ), |
| 294 | ), |
| 295 | ); |
| 296 | |
| 297 | final Offset location = tester.getCenter(find.text('test' )); |
| 298 | final List<PointerEventRecord> records = <PointerEventRecord>[ |
| 299 | PointerEventRecord(Duration.zero, <PointerEvent>[ |
| 300 | // Typically PointerAddedEvent is not used in testers, but for records |
| 301 | // captured on a device it is usually what start a gesture. |
| 302 | PointerAddedEvent(position: location), |
| 303 | PointerDownEvent(position: location, buttons: kSecondaryMouseButton, pointer: 1), |
| 304 | ]), |
| 305 | ...<PointerEventRecord>[ |
| 306 | for ( |
| 307 | Duration t = const Duration(milliseconds: 5); |
| 308 | t < const Duration(milliseconds: 80); |
| 309 | t += const Duration(milliseconds: 16) |
| 310 | ) |
| 311 | PointerEventRecord(t, <PointerEvent>[ |
| 312 | PointerMoveEvent( |
| 313 | timeStamp: t - const Duration(milliseconds: 1), |
| 314 | position: location, |
| 315 | buttons: kSecondaryMouseButton, |
| 316 | pointer: 1, |
| 317 | ), |
| 318 | ]), |
| 319 | ], |
| 320 | PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[ |
| 321 | PointerUpEvent( |
| 322 | timeStamp: const Duration(milliseconds: 79), |
| 323 | position: location, |
| 324 | buttons: kSecondaryMouseButton, |
| 325 | pointer: 1, |
| 326 | ), |
| 327 | ]), |
| 328 | ]; |
| 329 | final List<Duration> timeDiffs = await tester.handlePointerEventRecord(records); |
| 330 | expect(timeDiffs.length, records.length); |
| 331 | for (final Duration diff in timeDiffs) { |
| 332 | expect(diff, Duration.zero); |
| 333 | } |
| 334 | |
| 335 | const String b = ' $kSecondaryMouseButton' ; |
| 336 | expect(logs.first, 'down $b' ); |
| 337 | for (int i = 1; i < logs.length - 1; i++) { |
| 338 | expect(logs[i], 'move $b' ); |
| 339 | } |
| 340 | expect(logs.last, 'up $b' ); |
| 341 | }); |
| 342 | |
| 343 | group('runAsync' , () { |
| 344 | testWidgets('works with no async calls' , (WidgetTester tester) async { |
| 345 | String? value; |
| 346 | await tester.runAsync(() async { |
| 347 | value = '123' ; |
| 348 | }); |
| 349 | expect(value, '123' ); |
| 350 | }); |
| 351 | |
| 352 | testWidgets('works with real async calls' , (WidgetTester tester) async { |
| 353 | final StringBuffer buf = StringBuffer('1' ); |
| 354 | await tester.runAsync(() async { |
| 355 | buf.write('2' ); |
| 356 | //ignore: avoid_slow_async_io |
| 357 | await Directory.current.stat(); |
| 358 | buf.write('3' ); |
| 359 | }); |
| 360 | buf.write('4' ); |
| 361 | expect(buf.toString(), '1234' ); |
| 362 | }); |
| 363 | |
| 364 | testWidgets('propagates return values' , (WidgetTester tester) async { |
| 365 | final String? value = await tester.runAsync<String>(() async { |
| 366 | return '123' ; |
| 367 | }); |
| 368 | expect(value, '123' ); |
| 369 | }); |
| 370 | |
| 371 | testWidgets('reports errors via framework' , (WidgetTester tester) async { |
| 372 | final String? value = await tester.runAsync<String>(() async { |
| 373 | throw ArgumentError(); |
| 374 | }); |
| 375 | expect(value, isNull); |
| 376 | expect(tester.takeException(), isArgumentError); |
| 377 | }); |
| 378 | |
| 379 | testWidgets('disallows re-entry' , (WidgetTester tester) async { |
| 380 | final Completer<void> completer = Completer<void>(); |
| 381 | tester.runAsync<void>(() => completer.future); |
| 382 | expect(() => tester.runAsync(() async {}), throwsA(isA<TestFailure>())); |
| 383 | completer.complete(); |
| 384 | }); |
| 385 | |
| 386 | testWidgets('maintains existing zone values' , (WidgetTester tester) async { |
| 387 | final Object key = Object(); |
| 388 | await runZoned<Future<void>>(() { |
| 389 | expect(Zone.current[key], 'abczed' ); |
| 390 | return tester.runAsync<void>(() async { |
| 391 | expect(Zone.current[key], 'abczed' ); |
| 392 | }); |
| 393 | }, zoneValues: <dynamic, dynamic>{key: 'abczed' }); |
| 394 | }); |
| 395 | |
| 396 | testWidgets('control test (return value)' , (WidgetTester tester) async { |
| 397 | final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner' ); |
| 398 | expect(result, 'Judy Turner' ); |
| 399 | }); |
| 400 | |
| 401 | testWidgets('async throw' , (WidgetTester tester) async { |
| 402 | final String? result = await tester.binding.runAsync<Never>( |
| 403 | () async => throw Exception('Lois Dilettente' ), |
| 404 | ); |
| 405 | expect(result, isNull); |
| 406 | expect(tester.takeException(), isNotNull); |
| 407 | }); |
| 408 | |
| 409 | testWidgets('sync throw' , (WidgetTester tester) async { |
| 410 | final String? result = await tester.binding.runAsync<Never>( |
| 411 | () => throw Exception('Butch Barton' ), |
| 412 | ); |
| 413 | expect(result, isNull); |
| 414 | expect(tester.takeException(), isNotNull); |
| 415 | }); |
| 416 | |
| 417 | testWidgets('runs in original zone' , (WidgetTester tester) async { |
| 418 | final Zone testZone = Zone.current; |
| 419 | Zone? runAsyncZone; |
| 420 | Zone? timerZone; |
| 421 | Zone? periodicTimerZone; |
| 422 | Zone? microtaskZone; |
| 423 | |
| 424 | Zone? innerZone; |
| 425 | Zone? innerTimerZone; |
| 426 | Zone? innerPeriodicTimerZone; |
| 427 | Zone? innerMicrotaskZone; |
| 428 | |
| 429 | await tester.binding.runAsync<void>(() async { |
| 430 | final Zone currentZone = Zone.current; |
| 431 | runAsyncZone = currentZone; |
| 432 | |
| 433 | // Complete a future when all callbacks have completed. |
| 434 | int pendingCallbacks = 6; |
| 435 | final Completer<void> callbacksDone = Completer<void>(); |
| 436 | void onCallback() { |
| 437 | if (--pendingCallbacks == 0) { |
| 438 | testZone.run(() { |
| 439 | callbacksDone.complete(null); |
| 440 | }); |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | // On the runAsync zone itself. |
| 445 | currentZone.createTimer(Duration.zero, () { |
| 446 | timerZone = Zone.current; |
| 447 | onCallback(); |
| 448 | }); |
| 449 | currentZone.createPeriodicTimer(Duration.zero, (Timer timer) { |
| 450 | timer.cancel(); |
| 451 | periodicTimerZone = Zone.current; |
| 452 | onCallback(); |
| 453 | }); |
| 454 | currentZone.scheduleMicrotask(() { |
| 455 | microtaskZone = Zone.current; |
| 456 | onCallback(); |
| 457 | }); |
| 458 | |
| 459 | // On a nested user-created zone. |
| 460 | final Zone inner = runZoned(() => Zone.current); |
| 461 | innerZone = inner; |
| 462 | inner.createTimer(Duration.zero, () { |
| 463 | innerTimerZone = Zone.current; |
| 464 | onCallback(); |
| 465 | }); |
| 466 | inner.createPeriodicTimer(Duration.zero, (Timer timer) { |
| 467 | timer.cancel(); |
| 468 | innerPeriodicTimerZone = Zone.current; |
| 469 | onCallback(); |
| 470 | }); |
| 471 | inner.scheduleMicrotask(() { |
| 472 | innerMicrotaskZone = Zone.current; |
| 473 | onCallback(); |
| 474 | }); |
| 475 | |
| 476 | await callbacksDone.future; |
| 477 | }); |
| 478 | expect(runAsyncZone, isNotNull); |
| 479 | expect(timerZone, same(runAsyncZone)); |
| 480 | expect(periodicTimerZone, same(runAsyncZone)); |
| 481 | expect(microtaskZone, same(runAsyncZone)); |
| 482 | |
| 483 | expect(innerZone, isNotNull); |
| 484 | expect(innerTimerZone, same(innerZone)); |
| 485 | expect(innerPeriodicTimerZone, same(innerZone)); |
| 486 | expect(innerMicrotaskZone, same(innerZone)); |
| 487 | |
| 488 | expect(runAsyncZone, isNot(same(testZone))); |
| 489 | expect(runAsyncZone, isNot(same(innerZone))); |
| 490 | expect(innerZone, isNot(same(testZone))); |
| 491 | }); |
| 492 | }); |
| 493 | |
| 494 | group('showKeyboard' , () { |
| 495 | testWidgets('can be called twice' , (WidgetTester tester) async { |
| 496 | await tester.pumpWidget( |
| 497 | MaterialApp( |
| 498 | home: Material(child: Center(child: TextFormField())), |
| 499 | ), |
| 500 | ); |
| 501 | await tester.showKeyboard(find.byType(TextField)); |
| 502 | await tester.testTextInput.receiveAction(TextInputAction.done); |
| 503 | await tester.pump(); |
| 504 | await tester.showKeyboard(find.byType(TextField)); |
| 505 | await tester.testTextInput.receiveAction(TextInputAction.done); |
| 506 | await tester.pump(); |
| 507 | await tester.showKeyboard(find.byType(TextField)); |
| 508 | await tester.showKeyboard(find.byType(TextField)); |
| 509 | await tester.pump(); |
| 510 | }); |
| 511 | |
| 512 | testWidgets( |
| 513 | 'can focus on offstage text input field if finder says not to skip offstage nodes' , |
| 514 | (WidgetTester tester) async { |
| 515 | await tester.pumpWidget( |
| 516 | MaterialApp( |
| 517 | home: Material(child: Offstage(child: TextFormField())), |
| 518 | ), |
| 519 | ); |
| 520 | await tester.showKeyboard(find.byType(TextField, skipOffstage: false)); |
| 521 | }, |
| 522 | ); |
| 523 | }); |
| 524 | |
| 525 | testWidgets('verifyTickersWereDisposed control test' , (WidgetTester tester) async { |
| 526 | late FlutterError error; |
| 527 | final Ticker ticker = tester.createTicker((Duration duration) {}); |
| 528 | ticker.start(); |
| 529 | try { |
| 530 | tester.verifyTickersWereDisposed('' ); |
| 531 | } on FlutterError catch (e) { |
| 532 | error = e; |
| 533 | } finally { |
| 534 | expect(error, isNotNull); |
| 535 | expect(error.diagnostics.length, 4); |
| 536 | expect(error.diagnostics[2].level, DiagnosticLevel.hint); |
| 537 | expect( |
| 538 | error.diagnostics[2].toStringDeep(), |
| 539 | 'Tickers used by AnimationControllers should be disposed by\n' |
| 540 | 'calling dispose() on the AnimationController itself. Otherwise,\n' |
| 541 | 'the ticker will leak.\n' , |
| 542 | ); |
| 543 | expect(error.diagnostics.last, isA<DiagnosticsProperty<Ticker>>()); |
| 544 | expect(error.diagnostics.last.value, ticker); |
| 545 | expect( |
| 546 | error.toStringDeep(), |
| 547 | startsWith( |
| 548 | 'FlutterError\n' |
| 549 | ' A Ticker was active .\n' |
| 550 | ' All Tickers must be disposed.\n' |
| 551 | ' Tickers used by AnimationControllers should be disposed by\n' |
| 552 | ' calling dispose() on the AnimationController itself. Otherwise,\n' |
| 553 | ' the ticker will leak.\n' |
| 554 | ' The offending ticker was:\n' |
| 555 | ' _TestTicker()\n' , |
| 556 | ), |
| 557 | ); |
| 558 | } |
| 559 | ticker.stop(); |
| 560 | }); |
| 561 | |
| 562 | group('testWidgets variants work' , () { |
| 563 | int numberOfVariationsRun = 0; |
| 564 | |
| 565 | testWidgets( |
| 566 | 'variant tests run all values provided' , |
| 567 | (WidgetTester tester) async { |
| 568 | if (debugDefaultTargetPlatformOverride == null) { |
| 569 | expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); |
| 570 | } else { |
| 571 | numberOfVariationsRun += 1; |
| 572 | } |
| 573 | }, |
| 574 | variant: TargetPlatformVariant(TargetPlatform.values.toSet()), |
| 575 | ); |
| 576 | |
| 577 | testWidgets( |
| 578 | 'variant tests have descriptions with details' , |
| 579 | (WidgetTester tester) async { |
| 580 | if (debugDefaultTargetPlatformOverride == null) { |
| 581 | expect(tester.testDescription, equals('variant tests have descriptions with details' )); |
| 582 | } else { |
| 583 | expect( |
| 584 | tester.testDescription, |
| 585 | equals( |
| 586 | 'variant tests have descriptions with details (variant: $debugDefaultTargetPlatformOverride)' , |
| 587 | ), |
| 588 | ); |
| 589 | } |
| 590 | }, |
| 591 | variant: TargetPlatformVariant(TargetPlatform.values.toSet()), |
| 592 | ); |
| 593 | }); |
| 594 | |
| 595 | group('TargetPlatformVariant' , () { |
| 596 | int numberOfVariationsRun = 0; |
| 597 | TargetPlatform? origTargetPlatform; |
| 598 | |
| 599 | setUpAll(() { |
| 600 | origTargetPlatform = debugDefaultTargetPlatformOverride; |
| 601 | }); |
| 602 | |
| 603 | tearDownAll(() { |
| 604 | expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform)); |
| 605 | }); |
| 606 | |
| 607 | testWidgets( |
| 608 | 'TargetPlatformVariant.only tests given value' , |
| 609 | (WidgetTester tester) async { |
| 610 | expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS)); |
| 611 | expect(defaultTargetPlatform, equals(TargetPlatform.iOS)); |
| 612 | }, |
| 613 | variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
| 614 | ); |
| 615 | |
| 616 | group('all' , () { |
| 617 | testWidgets('TargetPlatformVariant.all tests run all variants' , (WidgetTester tester) async { |
| 618 | if (debugDefaultTargetPlatformOverride == null) { |
| 619 | expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); |
| 620 | } else { |
| 621 | numberOfVariationsRun += 1; |
| 622 | } |
| 623 | }, variant: TargetPlatformVariant.all()); |
| 624 | |
| 625 | const Set<TargetPlatform> excludePlatforms = <TargetPlatform>{ |
| 626 | TargetPlatform.android, |
| 627 | TargetPlatform.linux, |
| 628 | }; |
| 629 | testWidgets( |
| 630 | 'TargetPlatformVariant.all, excluding runs an all variants except those provided in excluding' , |
| 631 | (WidgetTester tester) async { |
| 632 | if (debugDefaultTargetPlatformOverride == null) { |
| 633 | expect( |
| 634 | numberOfVariationsRun, |
| 635 | equals(TargetPlatform.values.length - excludePlatforms.length), |
| 636 | ); |
| 637 | expect( |
| 638 | excludePlatforms, |
| 639 | isNot(contains(debugDefaultTargetPlatformOverride)), |
| 640 | reason: 'this test should not run on any platform in excludePlatforms' , |
| 641 | ); |
| 642 | } else { |
| 643 | numberOfVariationsRun += 1; |
| 644 | } |
| 645 | }, |
| 646 | variant: TargetPlatformVariant.all(excluding: excludePlatforms), |
| 647 | ); |
| 648 | }); |
| 649 | |
| 650 | testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values' , ( |
| 651 | WidgetTester tester, |
| 652 | ) async { |
| 653 | final TargetPlatformVariant all = TargetPlatformVariant.all(); |
| 654 | final TargetPlatformVariant desktop = TargetPlatformVariant.all(); |
| 655 | final TargetPlatformVariant mobile = TargetPlatformVariant.all(); |
| 656 | expect(desktop.values.union(mobile.values), equals(all.values)); |
| 657 | }); |
| 658 | }); |
| 659 | |
| 660 | group('Pending timer' , () { |
| 661 | late TestExceptionReporter currentExceptionReporter; |
| 662 | setUp(() { |
| 663 | currentExceptionReporter = reportTestException; |
| 664 | }); |
| 665 | |
| 666 | tearDown(() { |
| 667 | reportTestException = currentExceptionReporter; |
| 668 | }); |
| 669 | |
| 670 | test('Throws assertion message without code' , () async { |
| 671 | late FlutterErrorDetails flutterErrorDetails; |
| 672 | reportTestException = (FlutterErrorDetails details, String testDescription) { |
| 673 | flutterErrorDetails = details; |
| 674 | }; |
| 675 | |
| 676 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| 677 | debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED' ); |
| 678 | await binding.runTest(() async { |
| 679 | final Timer timer = Timer(const Duration(seconds: 1), () {}); |
| 680 | expect(timer.isActive, true); |
| 681 | }, () {}); |
| 682 | |
| 683 | expect(flutterErrorDetails.exception, isA<AssertionError>()); |
| 684 | expect( |
| 685 | (flutterErrorDetails.exception as AssertionError).message, |
| 686 | 'A Timer is still pending even after the widget tree was disposed.' , |
| 687 | ); |
| 688 | expect(binding.inTest, true); |
| 689 | binding.postTest(); |
| 690 | }); |
| 691 | }); |
| 692 | |
| 693 | group('Accessibility announcements testing API' , () { |
| 694 | testWidgets('Returns the list of announcements' , (WidgetTester tester) async { |
| 695 | // Make sure the handler is properly set |
| 696 | expect( |
| 697 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( |
| 698 | SystemChannels.accessibility.name, |
| 699 | null, |
| 700 | ), |
| 701 | isFalse, |
| 702 | ); |
| 703 | |
| 704 | await SemanticsService.announce('announcement 1' , TextDirection.ltr); |
| 705 | await SemanticsService.announce( |
| 706 | 'announcement 2' , |
| 707 | TextDirection.rtl, |
| 708 | assertiveness: Assertiveness.assertive, |
| 709 | ); |
| 710 | await SemanticsService.announce('announcement 3' , TextDirection.rtl); |
| 711 | |
| 712 | final List<CapturedAccessibilityAnnouncement> list = tester.takeAnnouncements(); |
| 713 | expect(list, hasLength(3)); |
| 714 | final CapturedAccessibilityAnnouncement first = list[0]; |
| 715 | expect(first.message, 'announcement 1' ); |
| 716 | expect(first.textDirection, TextDirection.ltr); |
| 717 | |
| 718 | final CapturedAccessibilityAnnouncement second = list[1]; |
| 719 | expect(second.message, 'announcement 2' ); |
| 720 | expect(second.textDirection, TextDirection.rtl); |
| 721 | expect(second.assertiveness, Assertiveness.assertive); |
| 722 | |
| 723 | final CapturedAccessibilityAnnouncement third = list[2]; |
| 724 | expect(third.message, 'announcement 3' ); |
| 725 | expect(third.textDirection, TextDirection.rtl); |
| 726 | expect(third.assertiveness, Assertiveness.polite); |
| 727 | |
| 728 | final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements(); |
| 729 | expect(emptyList, <CapturedAccessibilityAnnouncement>[]); |
| 730 | }); |
| 731 | |
| 732 | test('New test API is not breaking existing tests' , () async { |
| 733 | final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[]; |
| 734 | |
| 735 | Future<dynamic> handleMessage(dynamic mockMessage) async { |
| 736 | final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>; |
| 737 | log.add(message); |
| 738 | } |
| 739 | |
| 740 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
| 741 | .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage); |
| 742 | |
| 743 | await SemanticsService.announce( |
| 744 | 'announcement 1' , |
| 745 | TextDirection.rtl, |
| 746 | assertiveness: Assertiveness.assertive, |
| 747 | ); |
| 748 | expect( |
| 749 | log, |
| 750 | equals(<Map<String, dynamic>>[ |
| 751 | <String, dynamic>{ |
| 752 | 'type' : 'announce' , |
| 753 | 'data' : <String, dynamic>{ |
| 754 | 'message' : 'announcement 1' , |
| 755 | 'textDirection' : 0, |
| 756 | 'assertiveness' : 1, |
| 757 | }, |
| 758 | }, |
| 759 | ]), |
| 760 | ); |
| 761 | |
| 762 | // Remove the handler |
| 763 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
| 764 | .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); |
| 765 | }); |
| 766 | |
| 767 | tearDown(() { |
| 768 | // Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest] |
| 769 | expect( |
| 770 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( |
| 771 | SystemChannels.accessibility.name, |
| 772 | null, |
| 773 | ), |
| 774 | isTrue, |
| 775 | ); |
| 776 | }); |
| 777 | }); |
| 778 | |
| 779 | testWidgets('wrapWithView: false does not include View' , (WidgetTester tester) async { |
| 780 | FlutterView? flutterView; |
| 781 | View? view; |
| 782 | int builderCount = 0; |
| 783 | await tester.pumpWidget( |
| 784 | wrapWithView: false, |
| 785 | Builder( |
| 786 | builder: (BuildContext context) { |
| 787 | builderCount++; |
| 788 | flutterView = View.maybeOf(context); |
| 789 | view = context.findAncestorWidgetOfExactType<View>(); |
| 790 | return const ViewCollection(views: <Widget>[]); |
| 791 | }, |
| 792 | ), |
| 793 | ); |
| 794 | |
| 795 | expect(builderCount, 1); |
| 796 | expect(view, isNull); |
| 797 | expect(flutterView, isNull); |
| 798 | expect(find.byType(View), findsNothing); |
| 799 | }); |
| 800 | |
| 801 | testWidgets('passing a view to pumpWidget with wrapWithView: true throws' , ( |
| 802 | WidgetTester tester, |
| 803 | ) async { |
| 804 | await tester.pumpWidget(View(view: FakeView(tester.view), child: const SizedBox.shrink())); |
| 805 | expect( |
| 806 | tester.takeException(), |
| 807 | isFlutterError.having( |
| 808 | (FlutterError e) => e.message, |
| 809 | 'message' , |
| 810 | contains('consider setting the "wrapWithView" parameter of that method to false' ), |
| 811 | ), |
| 812 | ); |
| 813 | }); |
| 814 | |
| 815 | testWidgets('can pass a View to pumpWidget when wrapWithView: false' , ( |
| 816 | WidgetTester tester, |
| 817 | ) async { |
| 818 | await tester.pumpWidget( |
| 819 | wrapWithView: false, |
| 820 | View(view: tester.view, child: const SizedBox.shrink()), |
| 821 | ); |
| 822 | expect(find.byType(View), findsOne); |
| 823 | }); |
| 824 | } |
| 825 | |
| 826 | class FakeMatcher extends AsyncMatcher { |
| 827 | FakeMatcher(this.completer); |
| 828 | |
| 829 | final Completer<void> completer; |
| 830 | |
| 831 | @override |
| 832 | Future<String?> matchAsync(dynamic object) { |
| 833 | return completer.future.then<String?>((void value) { |
| 834 | return object?.toString(); |
| 835 | }); |
| 836 | } |
| 837 | |
| 838 | @override |
| 839 | Description describe(Description description) => description.add('--fake--' ); |
| 840 | } |
| 841 | |
| 842 | class _AlwaysAnimating extends StatefulWidget { |
| 843 | const _AlwaysAnimating({required this.onPaint}); |
| 844 | |
| 845 | final VoidCallback onPaint; |
| 846 | |
| 847 | @override |
| 848 | State<StatefulWidget> createState() => _AlwaysAnimatingState(); |
| 849 | } |
| 850 | |
| 851 | class _AlwaysAnimatingState extends State<_AlwaysAnimating> with SingleTickerProviderStateMixin { |
| 852 | late AnimationController _controller; |
| 853 | |
| 854 | @override |
| 855 | void initState() { |
| 856 | super.initState(); |
| 857 | _controller = AnimationController(duration: const Duration(milliseconds: 100), vsync: this); |
| 858 | _controller.repeat(); |
| 859 | } |
| 860 | |
| 861 | @override |
| 862 | void dispose() { |
| 863 | _controller.dispose(); |
| 864 | super.dispose(); |
| 865 | } |
| 866 | |
| 867 | @override |
| 868 | Widget build(BuildContext context) { |
| 869 | return AnimatedBuilder( |
| 870 | animation: _controller.view, |
| 871 | builder: (BuildContext context, Widget? child) { |
| 872 | return CustomPaint(painter: _AlwaysRepaint(widget.onPaint)); |
| 873 | }, |
| 874 | ); |
| 875 | } |
| 876 | } |
| 877 | |
| 878 | class _AlwaysRepaint extends CustomPainter { |
| 879 | _AlwaysRepaint(this.onPaint); |
| 880 | |
| 881 | final VoidCallback onPaint; |
| 882 | |
| 883 | @override |
| 884 | bool shouldRepaint(CustomPainter oldDelegate) => true; |
| 885 | |
| 886 | @override |
| 887 | void paint(Canvas canvas, Size size) { |
| 888 | onPaint(); |
| 889 | } |
| 890 | } |
| 891 | |