| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:flutter/cupertino.dart'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/scheduler.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:flutter_test/flutter_test.dart'; | |
| import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; | |
| import 'package:matcher/expect.dart' as matcher; | |
| import 'package:matcher/src/expect/async_matcher.dart'; | |
| import 'multi_view_testing.dart'; | |
| void main() { | |
| group('expectLater', () { | |
| testWidgets('completes when matcher completes', (WidgetTester tester) async { | |
| final completer = Completer<void>(); | |
| final Future<void> future = expectLater(null, FakeMatcher(completer)); | |
| String? result; | |
| // ignore: unawaited_futures | |
| future.then<void>((void value) { | |
| result = '123'; | |
| }); | |
| matcher.expect(result, isNull); | |
| completer.complete(); | |
| matcher.expect(result, isNull); | |
| await future; | |
| await tester.pump(); | |
| matcher.expect(result, '123'); | |
| }); | |
| testWidgets('respects the skip flag', (WidgetTester tester) async { | |
| final completer = Completer<void>(); | |
| final Future<void> future = expectLater( | |
| null, | |
| FakeMatcher(completer), | |
| skip: 'testing skip', | |
| ); // [intended] API testing | |
| var completed = false; | |
| // ignore: unawaited_futures | |
| future.then<void>((_) { | |
| completed = true; | |
| }); | |
| matcher.expect(completed, isFalse); | |
| await future; | |
| matcher.expect(completed, isTrue); | |
| }); | |
| }); | |
| group('group retry flag allows test to run multiple times', () { | |
| var retried = false; | |
| group('the group with retry flag', () { | |
| testWidgets('the test inside it', (WidgetTester tester) async { | |
| addTearDown(() => retried = true); | |
| if (!retried) { | |
| debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED'); | |
| } | |
| expect(retried, isTrue); | |
| }); | |
| }, retry: 1); | |
| }); | |
| group('testWidget retry flag allows test to run multiple times', () { | |
| var retried = false; | |
| testWidgets('the test with retry flag', (WidgetTester tester) async { | |
| addTearDown(() => retried = true); | |
| if (!retried) { | |
| debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED'); | |
| } | |
| expect(retried, isTrue); | |
| }, retry: 1); | |
| }); | |
| group('respects the group skip flag', () { | |
| testWidgets('should be skipped', (WidgetTester tester) async { | |
| expect(false, true); | |
| }); | |
| }, skip: true); // [intended] API testing | |
| group('pumping', () { | |
| testWidgets('pumping', (WidgetTester tester) async { | |
| await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); | |
| int count; | |
| final test = AnimationController(duration: const Duration(milliseconds: 5100), vsync: tester); | |
| addTearDown(test.dispose); | |
| count = await tester.pumpAndSettle(const Duration(seconds: 1)); | |
| expect(count, 1); // it always pumps at least one frame | |
| test.forward(from: 0.0); | |
| count = await tester.pumpAndSettle(const Duration(seconds: 1)); | |
| // 1 frame at t=0, starting the animation | |
| // 1 frame at t=1 | |
| // 1 frame at t=2 | |
| // 1 frame at t=3 | |
| // 1 frame at t=4 | |
| // 1 frame at t=5 | |
| // 1 frame at t=6, ending the animation | |
| expect(count, 7); | |
| test.forward(from: 0.0); | |
| await tester.pump(); // starts the animation | |
| count = await tester.pumpAndSettle(const Duration(seconds: 1)); | |
| expect(count, 6); | |
| test.forward(from: 0.0); | |
| await tester.pump(); // starts the animation | |
| await tester.pump(); // has no effect | |
| count = await tester.pumpAndSettle(const Duration(seconds: 1)); | |
| expect(count, 6); | |
| }); | |
| testWidgets('pumpFrames', (WidgetTester tester) async { | |
| final logPaints = <int>[]; | |
| int? initial; | |
| final Widget target = _AlwaysAnimating( | |
| onPaint: () { | |
| final int current = SchedulerBinding.instance.currentFrameTimeStamp.inMicroseconds; | |
| initial ??= current; | |
| logPaints.add(current - initial!); | |
| }, | |
| ); | |
| await tester.pumpFrames(target, const Duration(milliseconds: 55)); | |
| // `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump, | |
| // so we expect 4 pumps of 16683 microseconds each in the 55ms duration. | |
| expect(logPaints, <int>[0, 16683, 33366, 50049]); | |
| logPaints.clear(); | |
| await tester.pumpFrames( | |
| target, | |
| const Duration(milliseconds: 30), | |
| const Duration(milliseconds: 10), | |
| ); | |
| // Since `pumpFrames` was given a 10ms interval per pump, we expect the | |
| // results to continue from 50049 with 10000 microseconds per pump over | |
| // the 30ms duration. | |
| expect(logPaints, <int>[60049, 70049, 80049]); | |
| }); | |
| }); | |
| group('pageBack', () { | |
| testWidgets('fails when there are no back buttons', (WidgetTester tester) async { | |
| await tester.pumpWidget(Container()); | |
| expect(expectAsync0(tester.pageBack), throwsA(isA<TestFailure>())); | |
| }); | |
| testWidgets('successfully taps material back buttons', (WidgetTester tester) async { | |
| final observer = TransitionDurationObserver(); | |
| await tester.pumpWidget( | |
| MaterialApp( | |
| navigatorObservers: <NavigatorObserver>[observer], | |
| home: Center( | |
| child: Builder( | |
| builder: (BuildContext context) { | |
| return ElevatedButton( | |
| child: const Text('Next'), | |
| onPressed: () { | |
| Navigator.push<void>( | |
| context, | |
| MaterialPageRoute<void>( | |
| builder: (BuildContext context) { | |
| return Scaffold(appBar: AppBar(title: const Text('Page 2'))); | |
| }, | |
| ), | |
| ); | |
| }, | |
| ); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.tap(find.text('Next')); | |
| await tester.pump(); | |
| await tester.pump(observer.transitionDuration + const Duration(milliseconds: 1)); | |
| expect(find.text('Next'), findsNothing); | |
| expect(find.text('Page 2'), findsOneWidget); | |
| await tester.pageBack(); | |
| await tester.pump(); | |
| await tester.pump(observer.transitionDuration + const Duration(milliseconds: 1)); | |
| expect(find.text('Next'), findsOneWidget); | |
| expect(find.text('Page 2'), findsNothing); | |
| }); | |
| testWidgets('successfully taps cupertino back buttons', (WidgetTester tester) async { | |
| final observer = TransitionDurationObserver(); | |
| await tester.pumpWidget( | |
| MaterialApp( | |
| navigatorObservers: <NavigatorObserver>[observer], | |
| home: Center( | |
| child: Builder( | |
| builder: (BuildContext context) { | |
| return CupertinoButton( | |
| child: const Text('Next'), | |
| onPressed: () { | |
| Navigator.push<void>( | |
| context, | |
| CupertinoPageRoute<void>( | |
| builder: (BuildContext context) { | |
| return CupertinoPageScaffold( | |
| navigationBar: const CupertinoNavigationBar(middle: Text('Page 2')), | |
| child: Container(), | |
| ); | |
| }, | |
| ), | |
| ); | |
| }, | |
| ); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ); | |
| await tester.tap(find.text('Next')); | |
| await tester.pump(); | |
| await tester.pump(observer.transitionDuration); | |
| await tester.pageBack(); | |
| await tester.pump(); | |
| await tester.pumpAndSettle(); | |
| expect(find.text('Next'), findsOneWidget); | |
| expect(find.text('Page 2'), findsNothing); | |
| }); | |
| }); | |
| testWidgets('hasRunningAnimations control test', (WidgetTester tester) async { | |
| final controller = AnimationController( | |
| duration: const Duration(seconds: 1), | |
| vsync: const TestVSync(), | |
| ); | |
| addTearDown(controller.dispose); | |
| expect(tester.hasRunningAnimations, isFalse); | |
| controller.forward(); | |
| expect(tester.hasRunningAnimations, isTrue); | |
| controller.stop(); | |
| expect(tester.hasRunningAnimations, isFalse); | |
| controller.forward(); | |
| expect(tester.hasRunningAnimations, isTrue); | |
| await tester.pumpAndSettle(); | |
| expect(tester.hasRunningAnimations, isFalse); | |
| }); | |
| testWidgets( | |
| 'hasRunningAnimations is false when the transient callback does not schedule new frames', | |
| (WidgetTester tester) async { | |
| assert(!tester.hasRunningAnimations); | |
| tester.binding.scheduleFrameCallback((_) {}, scheduleNewFrame: false); | |
| expect(tester.hasRunningAnimations, false); | |
| }, | |
| ); | |
| testWidgets('pumpAndSettle control test', (WidgetTester tester) async { | |
| final controller = AnimationController( | |
| duration: const Duration(minutes: 525600), | |
| vsync: const TestVSync(), | |
| ); | |
| addTearDown(controller.dispose); | |
| expect(await tester.pumpAndSettle(), 1); | |
| controller.forward(); | |
| try { | |
| await tester.pumpAndSettle(); | |
| expect(true, isFalse); | |
| } catch (e) { | |
| expect(e, isFlutterError); | |
| } | |
| controller.stop(); | |
| expect(await tester.pumpAndSettle(), 1); | |
| controller.duration = const Duration(seconds: 1); | |
| controller.forward(); | |
| expect( | |
| await tester.pumpAndSettle(const Duration(milliseconds: 300)), | |
| 5, | |
| ); // 0, 300, 600, 900, 1200ms | |
| }); | |
| testWidgets('Input event array', (WidgetTester tester) async { | |
| final logs = <String>[]; | |
| await tester.pumpWidget( | |
| Directionality( | |
| textDirection: TextDirection.ltr, | |
| child: Listener( | |
| onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), | |
| onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), | |
| onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), | |
| child: const Text('test'), | |
| ), | |
| ), | |
| ); | |
| final Offset location = tester.getCenter(find.text('test')); | |
| final records = <PointerEventRecord>[ | |
| PointerEventRecord(Duration.zero, <PointerEvent>[ | |
| // Typically PointerAddedEvent is not used in testers, but for records | |
| // captured on a device it is usually what start a gesture. | |
| PointerAddedEvent(position: location), | |
| PointerDownEvent(position: location, buttons: kSecondaryMouseButton, pointer: 1), | |
| ]), | |
| ...<PointerEventRecord>[ | |
| for ( | |
| Duration t = const Duration(milliseconds: 5); | |
| t < const Duration(milliseconds: 80); | |
| t += const Duration(milliseconds: 16) | |
| ) | |
| PointerEventRecord(t, <PointerEvent>[ | |
| PointerMoveEvent( | |
| timeStamp: t - const Duration(milliseconds: 1), | |
| position: location, | |
| buttons: kSecondaryMouseButton, | |
| pointer: 1, | |
| ), | |
| ]), | |
| ], | |
| PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[ | |
| PointerUpEvent( | |
| timeStamp: const Duration(milliseconds: 79), | |
| position: location, | |
| buttons: kSecondaryMouseButton, | |
| pointer: 1, | |
| ), | |
| ]), | |
| ]; | |
| final List<Duration> timeDiffs = await tester.handlePointerEventRecord(records); | |
| expect(timeDiffs.length, records.length); | |
| for (final diff in timeDiffs) { | |
| expect(diff, Duration.zero); | |
| } | |
| const b = '$kSecondaryMouseButton'; | |
| expect(logs.first, 'down $b'); | |
| for (var i = 1; i < logs.length - 1; i++) { | |
| expect(logs[i], 'move $b'); | |
| } | |
| expect(logs.last, 'up $b'); | |
| }); | |
| group('runAsync', () { | |
| testWidgets('works with no async calls', (WidgetTester tester) async { | |
| String? value; | |
| await tester.runAsync(() async { | |
| value = '123'; | |
| }); | |
| expect(value, '123'); | |
| }); | |
| testWidgets('works with real async calls', (WidgetTester tester) async { | |
| final buf = StringBuffer('1'); | |
| await tester.runAsync(() async { | |
| buf.write('2'); | |
| //ignore: avoid_slow_async_io | |
| await Directory.current.stat(); | |
| buf.write('3'); | |
| }); | |
| buf.write('4'); | |
| expect(buf.toString(), '1234'); | |
| }); | |
| testWidgets('propagates return values', (WidgetTester tester) async { | |
| final String? value = await tester.runAsync<String>(() async { | |
| return '123'; | |
| }); | |
| expect(value, '123'); | |
| }); | |
| testWidgets('reports errors via framework', (WidgetTester tester) async { | |
| final String? value = await tester.runAsync<String>(() async { | |
| throw ArgumentError(); | |
| }); | |
| expect(value, isNull); | |
| expect(tester.takeException(), isArgumentError); | |
| }); | |
| testWidgets('disallows re-entry', (WidgetTester tester) async { | |
| // This test requires the first runAsync to not have finished | |
| // in order to test re-entry. | |
| final completer = Completer<void>(); | |
| // ignore: unawaited_futures | |
| tester.runAsync<void>(() => completer.future); | |
| expect(() => tester.runAsync(() async {}), throwsA(isA<TestFailure>())); | |
| completer.complete(); | |
| }); | |
| testWidgets('maintains existing zone values', (WidgetTester tester) async { | |
| final key = Object(); | |
| await runZoned<Future<void>>(() { | |
| expect(Zone.current[key], 'abczed'); | |
| return tester.runAsync<void>(() async { | |
| expect(Zone.current[key], 'abczed'); | |
| }); | |
| }, zoneValues: <dynamic, dynamic>{key: 'abczed'}); | |
| }); | |
| testWidgets('control test (return value)', (WidgetTester tester) async { | |
| final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner'); | |
| expect(result, 'Judy Turner'); | |
| }); | |
| testWidgets('async throw', (WidgetTester tester) async { | |
| final String? result = await tester.binding.runAsync<Never>( | |
| () async => throw Exception('Lois Dilettente'), | |
| ); | |
| expect(result, isNull); | |
| expect(tester.takeException(), isNotNull); | |
| }); | |
| testWidgets('sync throw', (WidgetTester tester) async { | |
| final String? result = await tester.binding.runAsync<Never>( | |
| () => throw Exception('Butch Barton'), | |
| ); | |
| expect(result, isNull); | |
| expect(tester.takeException(), isNotNull); | |
| }); | |
| testWidgets('runs in original zone', (WidgetTester tester) async { | |
| final Zone testZone = Zone.current; | |
| Zone? runAsyncZone; | |
| Zone? timerZone; | |
| Zone? periodicTimerZone; | |
| Zone? microtaskZone; | |
| Zone? innerZone; | |
| Zone? innerTimerZone; | |
| Zone? innerPeriodicTimerZone; | |
| Zone? innerMicrotaskZone; | |
| await tester.binding.runAsync<void>(() async { | |
| final Zone currentZone = Zone.current; | |
| runAsyncZone = currentZone; | |
| // Complete a future when all callbacks have completed. | |
| var pendingCallbacks = 6; | |
| final callbacksDone = Completer<void>(); | |
| void onCallback() { | |
| if (--pendingCallbacks == 0) { | |
| testZone.run(() { | |
| callbacksDone.complete(); | |
| }); | |
| } | |
| } | |
| // On the runAsync zone itself. | |
| currentZone.createTimer(Duration.zero, () { | |
| timerZone = Zone.current; | |
| onCallback(); | |
| }); | |
| currentZone.createPeriodicTimer(Duration.zero, (Timer timer) { | |
| timer.cancel(); | |
| periodicTimerZone = Zone.current; | |
| onCallback(); | |
| }); | |
| currentZone.scheduleMicrotask(() { | |
| microtaskZone = Zone.current; | |
| onCallback(); | |
| }); | |
| // On a nested user-created zone. | |
| final Zone inner = runZoned(() => Zone.current); | |
| innerZone = inner; | |
| inner.createTimer(Duration.zero, () { | |
| innerTimerZone = Zone.current; | |
| onCallback(); | |
| }); | |
| inner.createPeriodicTimer(Duration.zero, (Timer timer) { | |
| timer.cancel(); | |
| innerPeriodicTimerZone = Zone.current; | |
| onCallback(); | |
| }); | |
| inner.scheduleMicrotask(() { | |
| innerMicrotaskZone = Zone.current; | |
| onCallback(); | |
| }); | |
| await callbacksDone.future; | |
| }); | |
| expect(runAsyncZone, isNotNull); | |
| expect(timerZone, same(runAsyncZone)); | |
| expect(periodicTimerZone, same(runAsyncZone)); | |
| expect(microtaskZone, same(runAsyncZone)); | |
| expect(innerZone, isNotNull); | |
| expect(innerTimerZone, same(innerZone)); | |
| expect(innerPeriodicTimerZone, same(innerZone)); | |
| expect(innerMicrotaskZone, same(innerZone)); | |
| expect(runAsyncZone, isNot(same(testZone))); | |
| expect(runAsyncZone, isNot(same(innerZone))); | |
| expect(innerZone, isNot(same(testZone))); | |
| }); | |
| }); | |
| group('showKeyboard', () { | |
| testWidgets('can be called twice', (WidgetTester tester) async { | |
| await tester.pumpWidget( | |
| MaterialApp( | |
| home: Material(child: Center(child: TextFormField())), | |
| ), | |
| ); | |
| await tester.showKeyboard(find.byType(TextField)); | |
| await tester.testTextInput.receiveAction(TextInputAction.done); | |
| await tester.pump(); | |
| await tester.showKeyboard(find.byType(TextField)); | |
| await tester.testTextInput.receiveAction(TextInputAction.done); | |
| await tester.pump(); | |
| await tester.showKeyboard(find.byType(TextField)); | |
| await tester.showKeyboard(find.byType(TextField)); | |
| await tester.pump(); | |
| }); | |
| testWidgets( | |
| 'can focus on offstage text input field if finder says not to skip offstage nodes', | |
| (WidgetTester tester) async { | |
| await tester.pumpWidget( | |
| MaterialApp( | |
| home: Material(child: Offstage(child: TextFormField())), | |
| ), | |
| ); | |
| await tester.showKeyboard(find.byType(TextField, skipOffstage: false)); | |
| }, | |
| ); | |
| }); | |
| testWidgets('verifyTickersWereDisposed control test', (WidgetTester tester) async { | |
| late FlutterError error; | |
| final Ticker ticker = tester.createTicker((Duration duration) {}); | |
| addTearDown(ticker.dispose); | |
| ticker.start(); | |
| try { | |
| tester.verifyTickersWereDisposed(''); | |
| } on FlutterError catch (e) { | |
| error = e; | |
| } finally { | |
| expect(error, isNotNull); | |
| expect(error.diagnostics.length, 4); | |
| expect(error.diagnostics[2].level, DiagnosticLevel.hint); | |
| expect( | |
| error.diagnostics[2].toStringDeep(), | |
| 'Tickers used by AnimationControllers should be disposed by\n' | |
| 'calling dispose() on the AnimationController itself. Otherwise,\n' | |
| 'the ticker will leak.\n', | |
| ); | |
| expect(error.diagnostics.last, isA<DiagnosticsProperty<Ticker>>()); | |
| expect(error.diagnostics.last.value, ticker); | |
| expect( | |
| error.toStringDeep(), | |
| startsWith( | |
| 'FlutterError\n' | |
| ' A Ticker was active .\n' | |
| ' All Tickers must be disposed.\n' | |
| ' Tickers used by AnimationControllers should be disposed by\n' | |
| ' calling dispose() on the AnimationController itself. Otherwise,\n' | |
| ' the ticker will leak.\n' | |
| ' The offending ticker was:\n' | |
| ' _TestTicker()\n', | |
| ), | |
| ); | |
| } | |
| ticker.stop(); | |
| }); | |
| group('testWidgets variants work', () { | |
| var numberOfVariationsRun = 0; | |
| testWidgets('variant tests run all values provided', (WidgetTester tester) async { | |
| if (debugDefaultTargetPlatformOverride == null) { | |
| expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); | |
| } else { | |
| numberOfVariationsRun += 1; | |
| } | |
| }, variant: TargetPlatformVariant(TargetPlatform.values.toSet())); | |
| testWidgets('variant tests have descriptions with details', (WidgetTester tester) async { | |
| if (debugDefaultTargetPlatformOverride == null) { | |
| expect(tester.testDescription, equals('variant tests have descriptions with details')); | |
| } else { | |
| expect( | |
| tester.testDescription, | |
| equals( | |
| 'variant tests have descriptions with details (variant: $debugDefaultTargetPlatformOverride)', | |
| ), | |
| ); | |
| } | |
| }, variant: TargetPlatformVariant(TargetPlatform.values.toSet())); | |
| }); | |
| group('TargetPlatformVariant', () { | |
| var numberOfVariationsRun = 0; | |
| TargetPlatform? origTargetPlatform; | |
| setUpAll(() { | |
| origTargetPlatform = debugDefaultTargetPlatformOverride; | |
| }); | |
| tearDownAll(() { | |
| expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform)); | |
| }); | |
| testWidgets('TargetPlatformVariant.only tests given value', (WidgetTester tester) async { | |
| expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS)); | |
| expect(defaultTargetPlatform, equals(TargetPlatform.iOS)); | |
| }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); | |
| group('all', () { | |
| testWidgets('TargetPlatformVariant.all tests run all variants', (WidgetTester tester) async { | |
| if (debugDefaultTargetPlatformOverride == null) { | |
| expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); | |
| } else { | |
| numberOfVariationsRun += 1; | |
| } | |
| }, variant: TargetPlatformVariant.all()); | |
| const excludePlatforms = <TargetPlatform>{TargetPlatform.android, TargetPlatform.linux}; | |
| testWidgets( | |
| 'TargetPlatformVariant.all, excluding runs an all variants except those provided in excluding', | |
| (WidgetTester tester) async { | |
| if (debugDefaultTargetPlatformOverride == null) { | |
| expect( | |
| numberOfVariationsRun, | |
| equals(TargetPlatform.values.length - excludePlatforms.length), | |
| ); | |
| expect( | |
| excludePlatforms, | |
| isNot(contains(debugDefaultTargetPlatformOverride)), | |
| reason: 'this test should not run on any platform in excludePlatforms', | |
| ); | |
| } else { | |
| numberOfVariationsRun += 1; | |
| } | |
| }, | |
| variant: TargetPlatformVariant.all(excluding: excludePlatforms), | |
| ); | |
| }); | |
| testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values', ( | |
| WidgetTester tester, | |
| ) async { | |
| final all = TargetPlatformVariant.all(); | |
| final desktop = TargetPlatformVariant.all(); | |
| final mobile = TargetPlatformVariant.all(); | |
| expect(desktop.values.union(mobile.values), equals(all.values)); | |
| }); | |
| }); | |
| group('Pending timer', () { | |
| late TestExceptionReporter currentExceptionReporter; | |
| setUp(() { | |
| currentExceptionReporter = reportTestException; | |
| }); | |
| tearDown(() { | |
| reportTestException = currentExceptionReporter; | |
| }); | |
| test('Throws assertion message without code', () async { | |
| late FlutterErrorDetails flutterErrorDetails; | |
| reportTestException = (FlutterErrorDetails details, String testDescription) { | |
| flutterErrorDetails = details; | |
| }; | |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); | |
| debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED'); | |
| await binding.runTest(() async { | |
| final timer = Timer(const Duration(seconds: 1), () {}); | |
| expect(timer.isActive, true); | |
| }, () {}); | |
| expect(flutterErrorDetails.exception, isA<AssertionError>()); | |
| expect( | |
| (flutterErrorDetails.exception as AssertionError).message, | |
| 'A Timer is still pending even after the widget tree was disposed.', | |
| ); | |
| expect(binding.inTest, true); | |
| binding.postTest(); | |
| }); | |
| }); | |
| group('Accessibility announcements testing API', () { | |
| testWidgets('Returns the list of announcements', (WidgetTester tester) async { | |
| // Make sure the handler is properly set | |
| expect( | |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( | |
| SystemChannels.accessibility.name, | |
| null, | |
| ), | |
| isFalse, | |
| ); | |
| await SemanticsService.sendAnnouncement(tester.view, 'announcement 1', TextDirection.ltr); | |
| await SemanticsService.sendAnnouncement( | |
| tester.view, | |
| 'announcement 2', | |
| TextDirection.rtl, | |
| assertiveness: Assertiveness.assertive, | |
| ); | |
| await SemanticsService.sendAnnouncement(tester.view, 'announcement 3', TextDirection.rtl); | |
| expect(tester.takeAnnouncements(), [ | |
| isAccessibilityAnnouncement('announcement 1', textDirection: TextDirection.ltr), | |
| isAccessibilityAnnouncement( | |
| 'announcement 2', | |
| textDirection: TextDirection.rtl, | |
| assertiveness: Assertiveness.assertive, | |
| ), | |
| isAccessibilityAnnouncement( | |
| 'announcement 3', | |
| textDirection: TextDirection.rtl, | |
| assertiveness: Assertiveness.polite, | |
| ), | |
| ]); | |
| expect(tester.takeAnnouncements(), <CapturedAccessibilityAnnouncement>[]); | |
| }); | |
| testWidgets('New test API is not breaking existing tests', (WidgetTester tester) async { | |
| final log = <Map<dynamic, dynamic>>[]; | |
| Future<dynamic> handleMessage(dynamic mockMessage) async { | |
| final message = mockMessage as Map<dynamic, dynamic>; | |
| log.add(message); | |
| } | |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger | |
| .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage); | |
| await SemanticsService.sendAnnouncement( | |
| tester.view, | |
| 'announcement 1', | |
| TextDirection.rtl, | |
| assertiveness: Assertiveness.assertive, | |
| ); | |
| expect( | |
| log, | |
| equals(<Map<String, dynamic>>[ | |
| <String, dynamic>{ | |
| 'type': 'announce', | |
| 'data': <String, dynamic>{ | |
| 'viewId': 0, | |
| 'message': 'announcement 1', | |
| 'textDirection': 0, | |
| 'assertiveness': 1, | |
| }, | |
| }, | |
| ]), | |
| ); | |
| // Remove the handler | |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger | |
| .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); | |
| }); | |
| tearDown(() { | |
| // Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest] | |
| expect( | |
| TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( | |
| SystemChannels.accessibility.name, | |
| null, | |
| ), | |
| isTrue, | |
| ); | |
| }); | |
| }); | |
| testWidgets('wrapWithView: false does not include View', (WidgetTester tester) async { | |
| FlutterView? flutterView; | |
| View? view; | |
| var builderCount = 0; | |
| await tester.pumpWidget( | |
| wrapWithView: false, | |
| Builder( | |
| builder: (BuildContext context) { | |
| builderCount++; | |
| flutterView = View.maybeOf(context); | |
| view = context.findAncestorWidgetOfExactType<View>(); | |
| return const ViewCollection(views: <Widget>[]); | |
| }, | |
| ), | |
| ); | |
| expect(builderCount, 1); | |
| expect(view, isNull); | |
| expect(flutterView, isNull); | |
| expect(find.byType(View), findsNothing); | |
| }); | |
| testWidgets( | |
| 'passing a view to pumpWidget with wrapWithView: true throws', | |
| experimentalLeakTesting: LeakTesting.settings | |
| .withIgnoredAll(), // leaking by design because of exception | |
| (WidgetTester tester) async { | |
| await tester.pumpWidget(View(view: FakeView(tester.view), child: const SizedBox.shrink())); | |
| expect( | |
| tester.takeException(), | |
| isFlutterError.having( | |
| (FlutterError e) => e.message, | |
| 'message', | |
| contains('consider setting the "wrapWithView" parameter of that method to false'), | |
| ), | |
| ); | |
| }, | |
| ); | |
| testWidgets('can pass a View to pumpWidget when wrapWithView: false', ( | |
| WidgetTester tester, | |
| ) async { | |
| await tester.pumpWidget( | |
| wrapWithView: false, | |
| View(view: tester.view, child: const SizedBox.shrink()), | |
| ); | |
| expect(find.byType(View), findsOne); | |
| }); | |
| group('Leak tests', () { | |
| // Regression test for https://github.com/flutter/flutter/issues/169119. | |
| testWidgets('Does not leak if restorationManager is accessed', (WidgetTester tester) async { | |
| var counterByWidgets = 0; | |
| final RestorationManager managerByWidgets = WidgetsBinding.instance.restorationManager; | |
| expect(managerByWidgets, isA<TestRestorationManager>()); | |
| managerByWidgets.addListener(() => counterByWidgets++); | |
| managerByWidgets.notifyListeners(); | |
| expect(counterByWidgets, 1); | |
| var counterByServices = 0; | |
| final RestorationManager managerByServices = ServicesBinding.instance.restorationManager; | |
| expect(managerByServices, isA<TestRestorationManager>()); | |
| managerByServices.addListener(() => counterByServices++); | |
| managerByServices.notifyListeners(); | |
| expect(counterByServices, 1); | |
| // Passes if no leaks are detected. | |
| }); | |
| }); | |
| } | |
| class FakeMatcher extends AsyncMatcher { | |
| FakeMatcher(this.completer); | |
| final Completer<void> completer; | |
| @override | |
| Future<String?> matchAsync(dynamic object) { | |
| return completer.future.then<String?>((void value) { | |
| return object?.toString(); | |
| }); | |
| } | |
| @override | |
| Description describe(Description description) => description.add('--fake--'); | |
| } | |
| class _AlwaysAnimating extends StatefulWidget { | |
| const _AlwaysAnimating({required this.onPaint}); | |
| final VoidCallback onPaint; | |
| @override | |
| State<StatefulWidget> createState() => _AlwaysAnimatingState(); | |
| } | |
| class _AlwaysAnimatingState extends State<_AlwaysAnimating> with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: const Duration(milliseconds: 100), vsync: this); | |
| _controller.repeat(); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return AnimatedBuilder( | |
| animation: _controller.view, | |
| builder: (BuildContext context, Widget? child) { | |
| return CustomPaint(painter: _AlwaysRepaint(widget.onPaint)); | |
| }, | |
| ); | |
| } | |
| } | |
| class _AlwaysRepaint extends CustomPainter { | |
| _AlwaysRepaint(this.onPaint); | |
| final VoidCallback onPaint; | |
| @override | |
| bool shouldRepaint(CustomPainter oldDelegate) => true; | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| onPaint(); | |
| } | |
| } |