I'm stuck with a widget test and I could use some help
to reproduce the behavior please run the code sample below
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'home_page.dart';
void main() => runApp(
const ProviderScope(
child: MaterialApp(
home: Material(
child: MyHomePage(),
),
),
),
);
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
extension RoundX on double {
double roundToPrecision(int n) {
final f = pow(10, n);
return (this * f).round() / f;
}
}
final tasksPod = Provider<List<Future<void> Function()>>(
(ref) => [
for (var i = 0; i < 10; ++i)
() async {
await Future.delayed(kThemeAnimationDuration);
}
],
);
final progressPod = Provider.autoDispose<ValueNotifier<double>>((ref) {
final notifier = ValueNotifier<double>(0);
ref.onDispose(notifier.dispose);
return notifier;
});
class MyHomePage extends HookWidget {
const MyHomePage() : super(key: const ValueKey('MyHomePage'));
#override
Widget build(BuildContext context) {
final progress = useProvider(progressPod);
final tasks = useProvider(tasksPod);
useMemoized(() async {
final steps = tasks.length;
if (steps < 1) {
progress.value = 1;
} else {
for (final task in tasks) {
final current = progress.value;
if (current >= 1) {
break;
}
await task();
final value = (current + 1 / steps).roundToPrecision(1);
print('$value');
progress.value = value;
}
}
});
return Center(
child: ValueListenableBuilder<double>(
valueListenable: progress,
child: const FlutterLogo(),
builder: (context, value, child) =>
value < 1 ? const CircularProgressIndicator() : child!,
),
);
}
}
running the app everything is fine
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk... 4.7s
Syncing files to device Pixel 3a... 93ms
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
💪 Running with sound null safety 💪
An Observatory debugger and profiler on Pixel 3a is available at: http://127.0.0.1:36517/50vVndYZ3l4=/
I/flutter (19990): 0.1
I/flutter (19990): 0.2
I/flutter (19990): 0.3
I/flutter (19990): 0.4
I/flutter (19990): 0.5
I/flutter (19990): 0.6
I/flutter (19990): 0.7
The Flutter DevTools debugger and profiler on Pixel 3a is available at: http://127.0.0.1:9101?uri=http%3A%2F%2F127.0.0.1%3A36517%2F50vVndYZ3l4%3D%2F
I/flutter (19990): 0.8
I/flutter (19990): 0.9
I/flutter (19990): 1.0
Application finished.
but fails this test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:timeout_issue/home_page.dart';
void main() {
testWidgets(
'WHEN tasks are not completed'
'THEN shows `CircularProgressIndicator`', (tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
await tester.runAsync(() async {
await tester.pumpWidget(
ProviderScope(
child: const MaterialApp(
home: Material(
child: MyHomePage(),
),
),
),
);
await tester.pumpAndSettle(kThemeAnimationDuration);
expect(
find.byType(CircularProgressIndicator),
findsOneWidget,
reason: 'CircularProgressIndicator should be shown',
);
});
});
}
with this output
00:05 +0: WHEN tasks are not completedTHEN shows `CircularProgressIndicator`
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
pumpAndSettle timed out
When the exception was thrown, this was the stack:
#0 WidgetTester.pumpAndSettle.<anonymous closure> (package:flutter_test/src/widget_tester.dart:651:11)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:05 +0 -1: WHEN tasks are not completedTHEN shows `CircularProgressIndicator` [E]
Test failed. See exception logs above.
The test description was: WHEN tasks are not completedTHEN shows `CircularProgressIndicator`
00:05 +0 -1: Some tests failed.
the environment is
Flutter version 2.2.0-11.0.pre.176
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
hooks_riverpod: ^0.14.0
flutter_hooks: ^0.16.0
any help is apprecciated
I'd say the problem is related to using pumpAndSettle and an infinite animation (Circular progress indicator).
You can try using pump without the settle to build frames yourself.
https://api.flutter.dev/flutter/flutter_test/WidgetTester/pumpAndSettle.html
It seems atm riverpod and pumpAndSettle are not working, as a nasty quick hack you can try something like this:
for (int i = 0; i < 5; i++) {
// because pumpAndSettle doesn't work with riverpod
await tester.pump(Duration(seconds: 1));
}
#zuldyc is correct. By running the step asynchronously, it gives the Timer what it needs to finish successfully before continuing. I've got a working example now that will hopefully make things more clear.
BROKEN CODE
testWidgets('Testing Login Button Success - New User', (tester) async {
final amplifyAuthMock = MockAmplifyAuth();
final dbInterfaceMock = MockDatabaseInterface();
when(amplifyAuthMock.login('testNew#test.com', 'password!'))
.thenAnswer((result) async => true);
when(dbInterfaceMock.startStopDBSync())
.thenAnswer((realInvocation) async => true);
when(dbInterfaceMock.restartDBSync())
.thenAnswer((realInvocation) async => true);
// CREATING FORM TO TEST
await tester
.pumpWidget(createLoginForm(amplifyAuthMock, dbInterfaceMock));
await inputDummyLoginText(tester, email: 'testNew#test.com');
// PRESSING LOGIN BUTTON AND SHOULD GO TO HOME PAGE
await tester.tap(find.byType(SkillTreeElevatedButton));
// BREAKS HERE ON PUMP AND SETTLE******
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
It breaks because of the reasons described in accepted answer. Well, sort of. You get a sort of race condition because we are using a future which is asynchronous, but the code above doesn't account for that so it executes the future widget's code but does not know to wait for it to finish creating, so it exists and everything explodes. We need to make the ENTIRE process asynchronous. We do this by following Zuldyc's answer. By changing my code to the following it works without issue
// THE ABOVE CODE HAS NOT CHANGED, NEW CODE STARTS HERE
await tester
.runAsync(() => tester.tap(find.byType(SkillTreeElevatedButton)));
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
To be clear the change is as follows
//BEFORE
await tester.tap(find.byType(SkillTreeElevatedButton));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
//AFTER
await tester.runAsync(() => tester.tap(find.byType(SkillTreeElevatedButton)));
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
My tap action was triggering the new screen and the loading indicator, so i needed to make that action async so that it could finish.
it seems runAsync solves the issue
await tester.runAsync(() => tester.pumpWidget(
ProviderScope(child: MyApp()), const Duration(milliseconds: 100)));
final indicator = const CircularProgressIndicator();
await tester.pumpWidget(indicator);
expect(find.byWidget(indicator), findsOneWidget);
Related
** I am creating google map location app
I tried to resolve my self but i am not able to fix this bug
Please help me to fix this bug
i am getting error when i create isolate for get location
I have used packages
google_maps_flutter: ^2.1.8
geocoding: ^2.0.4
geolocator: ^8.2.1
flutter_bloc: ^8.0.1
**
i got error
> Restarted application in 2,738ms.
D/MapsInitializer( 6872): preferredRenderer: null
D/zzca ( 6872): preferredRenderer: null
I/Google Maps Android API( 6872): Google Play services package version: 221215028
I/Google Maps Android API( 6872): Google Play services maps renderer version(legacy): 203115000
7
I/Counters( 6872): exceeded sample count in FrameTime
>
> I/m.example.g_ma( 6872): NativeAlloc concurrent copying GC freed 17476(988KB) AllocSpace objects, 0(0B) LOS objects, 50% free, 7MB/14MB, paused 172us total 133.433ms
10
I/Counters( 6872): exceeded sample count in FrameTime
I/m.example.g_ma( 6872): NativeAlloc concurrent copying GC freed 4201(193KB) AllocSpace objects, 0(0B) LOS objects, 49% free, 7MB/14MB, paused 138us total 149.308ms
7
I/Counters( 6872): exceeded sample count in FrameTime
I/chatty ( 6872): uid=10498(com.example.g_map) androidmapsapi- identical 1 line
3
I/Counters( 6872): exceeded sample count in FrameTime
E/flutter ( 6872): [ERROR:flutter/runtime/dart_isolate.cc(1098)] Unhandled exception:
E/flutter ( 6872): Binding has not yet been initialized.
E/flutter ( 6872): The "instance" getter on the ServicesBinding binding mixin is only available once that binding has been initialized.
E/flutter ( 6872): Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will return the binding.
E/flutter ( 6872): In a test, one can call "TestWidgetsFlutterBinding.ensureInitialized()" as the first line in the test's "main()" method to initialize the binding.
E/flutter ( 6872): If ServicesBinding is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, but that mixes in the selected binding, and that is the class that must be constructed before using the "instance" getter.
E/flutter ( 6872): #0 BindingBase.checkInstance.<anonymous closure>
package:flutter/…/foundation/binding.dart:281
E/flutter ( 6872): #1 BindingBase.checkInstance
package:flutter/…/foundation/binding.dart:363
E/flutter ( 6872): #2 ServicesBinding.instance
package:flutter/…/services/binding.dart:48
E/flutter ( 6872): #3 MethodChannel.binaryMessenger
package:flutter/…/services/platform_channel.dart:132
**My Code is **
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:g_map/cubit/googlemap_cubit.dart';
// import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'screens/home_page.dart';
void main() async{
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return BlocProvider<GooglemapCubit>(
create: (context) => GooglemapCubit(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Map'),
),
);
}
}
import 'dart:developer';
import 'package:geolocator/geolocator.dart';
class GLocationPermission{
Future<bool> checkPermission()async{
bool serviceEnabled ;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if(!serviceEnabled){
log("",error: "Location service is not enabled");
}
permission = await Geolocator.checkPermission();
if(permission == LocationPermission.denied){
permission = await Geolocator.requestPermission();
if(permission == LocationPermission.denied){
log("Permission is denied again ");
}
}
if(permission == LocationPermission.deniedForever){
log("",error: "Location permission denied forever");
return false;
}
if(permission == LocationPermission.whileInUse || permission == LocationPermission.always){
return true;
}else{
return false;
}
}
}
import 'dart:developer';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:g_map/isolates/current_location_in_background.dart';
import 'package:g_map/services/location_permission.dart';
import 'package:geocoding/geocoding.dart';
// import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:geolocator/geolocator.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>{
late GoogleMapController _mapController;
final TextEditingController _searchAddressController = TextEditingController();
final _locationPermission = GLocationPermission();
String searchedAddress = "";
late ReceivePort _receivePort;
late Isolate _isolate;
#override
void initState() {
_checkPermission();
super.initState();
}
Future<void> _checkPermission()async{
if(await _locationPermission.checkPermission() == true){
createIsloate();
}
}
Future<void> createIsloate()async{
_receivePort = ReceivePort();
_isolate = await Isolate.spawn(CurrentLocationInBackGround.getLocation, _receivePort.sendPort);
_receivePort.listen((message) {
log("message $message");
// final pos = message as Stream;
// pos.listen((event) {
// log("Event $event");
// });
},
onError: (error){
log("Error $error",error: error);
}
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: [
_googleMap(
inititalCameraPosition: const CameraPosition(
target: LatLng(37.42796133580664, -122.085749655962),
)),
_searchAddressField(context: context)
],
));
}
Widget _googleMap({required CameraPosition inititalCameraPosition}) {
// buildWhen: (previous, current) => current.runtimeType == ,
return GoogleMap(
// minMaxZoomPreference: const MinMaxZoomPreference(1, 20),
zoomControlsEnabled: true,
onMapCreated: _onMapCreated,
myLocationButtonEnabled: true,
initialCameraPosition: inititalCameraPosition,
);
}
Widget _searchAddressField({required BuildContext context}){
return Card(
child: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height*.07,
child: Center(
child: TextField(
onChanged: (value){
// searchedAddress = context.read<GooglemapCubit>().searchAddress(address: value);
},
controller: _searchAddressController,
decoration: InputDecoration(border: InputBorder.none,
suffix: IconButton(onPressed:() => _searchAndNavigate(context: context), icon: const Icon(Icons.search))
),
),
),
),
);
}
Future<void> _searchAndNavigate({required BuildContext context})async{
final location = GeocodingPlatform.instance.locationFromAddress(_searchAddressController.text);
location.then((value){
debugPrint(value[0].latitude.toString());
debugPrint(value[0].longitude.toString());
_mapController.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
zoom: 12,
target:LatLng(value[0].latitude, value[0].longitude))));
});
}
void _onMapCreated(GoogleMapController controller) {
// _mapController = context.read<GooglemapCubit>().assignControllerOn(controller);
_mapController = controller;
}
}
import 'dart:developer';
import 'dart:isolate';
import 'package:geolocator/geolocator.dart';
class CurrentLocationInBackGround{
static void getLocation(SendPort sendPort)async{
final pos = await Geolocator.getCurrentPosition();
log("pos stream $pos");
sendPort.send(pos);
}
}
I think you tried to run geocoding methods on isolate because it's freezer the main thread
await placemarkFromCoordinates()
or
await locationFromAddress()
will freeze the main thread this bug on there repository
but here is a great solution
link to solution
I tried to test the behavior of future, but
Throwing an exception in future will break the dart main thread
However, the reverse is true in the Flutter:
Dart Code
Future(() async {
await Future.delayed(Duration(seconds: 10));
print("TimeOut");
}).timeout(Duration(seconds: 5));
Future(() async {
await Future.delayed(Duration(seconds: 15));
print("AAAAAAA");
});
in Dart use the code dart run ./a.dart
Unhandled exception:
TimeoutException after 0:00:05.000000: Future not completed
in Flutter:
E/flutter (11789): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: TimeoutException after 0:00:05.000000: Future not completed
E/flutter (11789):
I/flutter (11789): TimeOut
I/flutter (11789): AAAAAAA
Why ?
The difference between Dart and Flutter is that Flutter catches all unhandled exceptions and just prints them out (including the stack trace). This is on purpose and the secret is that Flutter registers a callback to the static class function FlutterError.onError and the default implementation is the function FlutterError.presentError. Some more details on the official docs: Handling errors in Flutter
Disable terminating on fatal errors in Dart
It's possible to disable terminating the app/script on fatal errors by calling Isolate.setErrorsFatal with false. For example, to keep async code running even if an unhandled exception is thrown just do this.
void main () {
Isolate.current.setErrorsFatal(false); // <- Here
Future(() async {
await Future.delayed(Duration(seconds: 10));
print("TimeOut");
}).timeout(Duration(seconds: 5));
Future(() async {
await Future.delayed(Duration(seconds: 15));
print("AAAAAAA");
});
}
And the output is going to be:
TimeOut
AAAAAAA
Dart with the same behaviour as Flutter
The following code simulates the same thing Flutter does in Dart with runZonedGuarded:
import 'dart:async';
void main(List<String> args) {
runZonedGuarded(program, errorHandler);
}
void program() {
Future(() async {
await Future.delayed(Duration(seconds: 10));
print("TimeOut");
}).timeout(Duration(seconds: 5));
Future(() async {
await Future.delayed(Duration(seconds: 15));
print("AAAAAAA");
});
}
void errorHandler(Object error, StackTrace stack) {
print('Unhandled exception:\n$error');
}
The output is going to be the following in Dart:
Unhandled exception:
TimeoutException after 0:00:05.000000: Future not completed
TimeOut
AAAAAAA
Flutter with the same behaviour as Dart
On the other hand, it's possible to do the same thing in Flutter as Dart does. I.e. kills the app on whenever unhandled exception. It's just a matter of using the runZonedGuarded again:
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
void main() {
runZonedGuarded(() async {
runApp(const MyApp());
}, (Object error, StackTrace stack) {
print('Unhandled exception:\n$error');
exit(1);
});
}
void program() {
Future(() async {
await Future.delayed(Duration(seconds: 10));
print("TimeOut");
}).timeout(Duration(seconds: 5));
Future(() async {
await Future.delayed(Duration(seconds: 15));
print("AAAAAAA");
});
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
program();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => const Scaffold(
body: Center(child: Text('The app is going to exit in 5 seconds...')),
),
);
}
}
The output is going to be the following:
flutter: Unhandled exception:
TimeoutException after 0:00:05.000000: Future not completed
Lost connection to device.
My app's MyApp widget, which returns MaterialApp (as done in virtually every Flutter app) is rebuild whenever the build function on the home widget is called. I need to know why this happens, as it greatly reduces the performance of my app.
I use a StreamProvider (the Riverpod implementation of StreamBuilder) to either show my app's HomePage, LandingPage or loading screen (called PseudoSplashScreen for historical reasons), depending on whether a user is logged in or not, or whether the stream is waiting.
My main.dart contains, among other things:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.instance.unsubscribeFromTopic('allUsers');
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
debugPrint("Returning MaterialApp");
return MaterialApp(
title: 'MyApp',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
debugShowCheckedModeBanner: false,
theme: themeDataLight(),
darkTheme: themeDataDark(),
home: const ReDirector(),
);
}
}
class ReDirector extends ConsumerWidget {
const ReDirector({Key? key}) : super(key: key);
static const LandingPage landingPage = LandingPage();
static const PseudoSplashScreen pseudoSplashScreen = PseudoSplashScreen();
#override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("Building Redirector");
return ref.watch(authStreamProvider).when(
data: (data) {
debugPrint(data.toString());
if (data != null && data == AuthResultStatus.successful) {
debugPrint("Returning Homepage");
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.blue,
);
} else {
debugPrint("AuthStreamProvider returned $data");
// When logging in, it is set to true. Hence, set it to false to prevent
// the isInAsync overlay from showing when logging out.
ref.read(landingPageProvider).isInAsync = false;
return landingPage;
}
},
error: (e, tb) {
debugPrint("Error in the AuthChecker");
debugPrint("$e\n$tb");
// When logging in, it is set to true. Hence, set it to false to prevent
// the isInAsync overlay from showing on error
ref.read(landingPageProvider).isInAsync = false;
return landingPage;
},
loading: () {
debugPrint("Returning PseudoSplashScreen");
return pseudoSplashScreen;
},
);
}
}
The Stream is derived from FirebaseAuth.instance.authStateChanges but is expanded to check some extra details on the user:
final authStreamProvider = StreamProvider.autoDispose<AuthResultStatus?>((ref) {
return FirebaseAuth.instance
.authStateChanges()
.asyncExpand((User? user) async* {
AuthResultStatus? result;
if (user != null) {
final IdTokenResult idTokenResult = await user.getIdTokenResult();
if (user.emailVerified && idTokenResult.claims!['approved'] == true) {
ref.read(userDataProvider).initialize(user, idTokenResult.claims!);
result = AuthResultStatus.successful;
} else {
result = AuthResultStatus.undefined;
}
}
debugPrint("AuthStreamProvider is yielding $result");
yield result;
});
});
Where AuthResultStatus is an enum. Now I would expect that while the stream is loading, the PseudoSplashScreen is shown, and when the Stream fires an AuthResultStatus.successful, the HomePage is shown. This is indeed what happens, but somehow my Redirector is rebuild about a second after the HomePage is shown. In fact, the build function of MyApp is called again! Regarding the debugPrints in the code, the console shows this:
I/flutter (22428): Returning MaterialApp
I/flutter (22428): Building Redirector
I/flutter (22428): Returning PseudoSplashScreen
I/flutter (22428): Creating new userdatamodel
I/flutter (22428): CURRENTUSER: Wessel van Dam
I/flutter (22428): AuthStreamProvider is yielding AuthResultStatus.successful
I/flutter (22428): Building Redirector
I/flutter (22428): AuthResultStatus.successful
I/flutter (22428): Returning Homepage
I/flutter (22428): Returning MaterialApp
I/flutter (22428): Building Redirector
I/flutter (22428): AuthResultStatus.successful
I/flutter (22428): Returning Homepage
Note that the rebuilding of the Redirector is not due to a new firing event of the Stream, because then you would expect another print of Returning successful ARS. However, this rebuild of the Redirector is pretty annoying as building the HomePage is a pretty intense process. This rebuild causes a the screen to flicker. Could anyone tell me why the Redirector's build function is called again in this sequence? If that can be prevented, the user experience for my app would be greatly improved.
When having multiple tests in the same file, and running one test after the other. Some kinds of tests are failing, but the same tests pass when run individually.
This is my test file at the moment, it's kinda long though:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rec/Components/Inputs/text_fields/DniTextField.dart';
import 'package:rec/Helpers/Validators.dart';
import '../../test_utils.dart';
void main() {
testWidgets('DniTextField works with invalid DNI', (WidgetTester tester) async {
var key = GlobalKey<DniTextFieldState>();
var formKey = GlobalKey<FormState>();
var onChangedResult;
var widget = Form(
key: formKey,
child: DniTextField(
key: key,
onChange: (String value) {
onChangedResult = value;
},
validator: (s) => Validators.verifyIdentityDocument(s),
),
);
await tester.pumpWidget(
TestUtils.wrapPublicRoute(widget),
);
await tester.pumpAndSettle();
// Test that widget has at least rendered
TestUtils.widgetExists(widget);
// Enter text into field with
var widgetFinder = find.byWidget(widget);
await tester.tap(widgetFinder);
await tester.showKeyboard(widgetFinder);
await tester.enterText(widgetFinder, 'invaliddni');
await tester.pumpAndSettle();
expect(onChangedResult, 'invaliddni');
expect(formKey.currentState.validate(), false);
});
testWidgets('DniTextField works with valid DNI', (WidgetTester tester) async {
var key = GlobalKey<DniTextFieldState>();
var formKey = GlobalKey<FormState>();
var onChangedResult;
var widget = Form(
key: formKey,
child: DniTextField(
key: key,
onChange: (String value) {
onChangedResult = value;
},
validator: (s) => Validators.verifyIdentityDocument(s),
),
);
await tester.pumpWidget(
TestUtils.wrapPublicRoute(widget),
);
await tester.pumpAndSettle();
// Enter text into field
var widgetFinder = find.byType(DniTextField);
await tester.tap(widgetFinder);
await tester.showKeyboard(widgetFinder);
await tester.enterText(widgetFinder, '80008000k');
await tester.pumpAndSettle();
expect(onChangedResult, '80008000k');
expect(formKey.currentState.validate(), true);
});
testWidgets('DniTextField works with valid DNI with trailing space', (WidgetTester tester) async {
var key = GlobalKey<DniTextFieldState>();
var formKey = GlobalKey<FormState>();
var onChangedResult;
var widget = Form(
key: formKey,
child: DniTextField(
key: key,
onChange: (String value) {
onChangedResult = value;
},
validator: (s) => Validators.verifyIdentityDocument(s),
),
);
await tester.pumpWidget(TestUtils.wrapPublicRoute(widget));
await tester.pumpAndSettle();
// Enter text into field
var widgetFinder = find.byWidget(widget);
await tester.tap(widgetFinder);
await tester.showKeyboard(widgetFinder);
await tester.enterText(widgetFinder, '80008000k ');
await tester.pumpAndSettle();
// The value emitted by the field, should be free of trailing whitespace
expect(onChangedResult, '80008000k');
expect(formKey.currentState.validate(), true);
});
}
If I run this like this:
$ flutter test test/Components/inputs/dni_text_field_test.dart
The first test passes, but the next 2 don't, spitting out the following error:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
The finder "zero widgets with the given widget
(DniTextField-[LabeledGlobalKey<DniTextFieldState>#0fb86]) (ignoring offstage widgets)" (used in a
call to "tap()") could not find any matching widgets.
When the exception was thrown, this was the stack:
#0 WidgetController._getElementPoint (package:flutter_test/src/controller.dart:902:7)
#1 WidgetController.getCenter (package:flutter_test/src/controller.dart:841:12)
#2 WidgetController.tap (package:flutter_test/src/controller.dart:273:18)
#3 main.<anonymous closure> (file:///[masked]/test/Components/inputs/dni_text_field_test.dart:99:18)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
If I then comment out the first test, then the second one passes but the third one does not. As I said the test pass if I run them individually.
I can't find any information about this, not even one thing. Maybe there is someone here that can guide me in the right direction. I'm a bit of a noob with Flutter widget testing, so I might be missing some important things.
The only solution I've found for now is having each test in a separate file. It's not ideal though, would be much better to contain related tests in the same file. Flutter has examples showing that multiple tests per file is allowed as it should be.
Flutter Version: 2.4.0-5.0.pre.87
Okay I found the solution and reason, fixed thanks to this issue and this SO question.
The main issue was the use of Localizations that loaded from JSON files. The solution was to wrap each test with tester.runAsync()
testWidgets('widget test 2', (WidgetTester tester) async {
await tester.runAsync(() async {
// tests
});
});
I suppose this trouble occure when different test are not craeting different instances of widgets (or they are conflicting in some other way), but there is a turnaround that might help you.
If you want to create a single button running all your tests - you can create a bash (or cmd on windows) file, that is running all your tests sequentially.
Example all_tests.cmd
dart test_1.dart
dart test_2.dart
dart test_3.dart
It is not the clearest solution, but it might work for first time.
bounty info: I'll accept your answer if:
isn't something along the line do this instead
the code sample is mostly unchanged
produce successful test, not just some quote from docs
doesn't need any extra package
[edit : 07/02/21] following Miyoyo#5957 on flutter community on
discord #iapicca Convert widget position to global, get width height, add both, and see if the resulting bottom right position is on screen? and using the following answers as reference:
test widget global position
test widget size
flutter_test dimensions issue
given the code sample below (also runnable on dartpad)
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
final _testKey = GlobalKey();
const _fabKey = ValueKey('fab');
final _onScreen = ValueNotifier<bool>(true);
void main() => runApp(_myApp);
const _myApp = MaterialApp(
home: Scaffold(
body: MyStage(),
floatingActionButton: MyFAB(),
),
);
class MyFAB extends StatelessWidget {
const MyFAB() : super(key: const ValueKey('MyFAB'));
#override
Widget build(BuildContext context) => FloatingActionButton(
key: _fabKey,
onPressed: () => _onScreen.value = !_onScreen.value,
);
}
class MyStage extends StatelessWidget {
const MyStage() : super(key: const ValueKey('MyStage'));
#override
Widget build(BuildContext context) => Stack(
children: [
ValueListenableBuilder(
child: FlutterLogo(
key: _testKey,
),
valueListenable: _onScreen,
builder: (context, isOnStage, child) => AnimatedPositioned(
top: MediaQuery.of(context).size.height *
(_onScreen.value ? .5 : -1),
child: child,
duration: const Duration(milliseconds: 100),
),
),
],
);
}
I want to test is the widget is off screen
here's the test code so far
void main() {
testWidgets('...', (tester) async {
await tester.pumpWidget(_myApp);
final rect = _testKey.currentContext.findRenderObject().paintBounds;
expect(tester.getSize(find.byKey(_testKey)), rect.size,
reason: 'size should match');
final lowestPointBefore = rect.bottomRight.dy;
print('lowest point **BEFORE** $lowestPointBefore ${DateTime.now()}');
expect(lowestPointBefore > .0, true, reason: 'should be on-screen');
await tester.tap(find.byKey(_fabKey));
await tester.pump(const Duration(milliseconds: 300));
final lowestPointAfter =
_testKey.currentContext.findRenderObject().paintBounds.bottomRight.dy;
print('lowest point **AFTER** $lowestPointAfter ${DateTime.now()}');
expect(lowestPointAfter > .0, false, reason: 'should be off-screen');
});
}
and the logs produced
00:03 +0: ...
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
Expected: <false>
Actual: <true>
When the exception was thrown, this was the stack:
#4 main.<anonymous closure> (file:///home/francesco/projects/issue/test/widget_test.dart:83:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
This was caught by the test expectation on the following line:
file:///home/francesco/projects/issue/test/widget_test.dart line 83
The test description was:
...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: ... [E]
Test failed. See exception logs above.
The test description was: ...
00:03 +0 -1: Some tests failed.
I'm not sure if my approach is correct
and the time in the print suggest me that
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
suggest me that
await tester.pumpAndSettle(Duration(milliseconds: 300));
doesn't do what I think it does
Problems are:
We were trying to find the rect of FlutterLogo but FlutterLogo rect will remain same the parent AnimatedPositioned widget's location are actually changing.
Even though we now start to check for AnimatedPositioned paintBounds it will still be the same as we are not changing width but the position it self.
Solution:
Get the screen rect by topWidget for me it's Scaffold. (if we have different widgets like HomeScreen which contains FAB button we just need to find that rect)
Before click I'm checking if fab button is on-screen or not
Tap and pump the widget and let it settle.
Search for widget rect and it will be out of the screen i.e. in our case -600
Added comments in the code it self
testWidgets('...', (tester) async {
await tester.pumpWidget(MyApp);
//check screen width height - here I'm checking for scaffold but you can put some other logic for screen size or parent widget type
Rect screenRect = tester.getRect(find.byType(Scaffold));
print("screenRect: $screenRect");
//checking previous position of the widget - on our case we are animating widget position via AnimatedPositioned
// which in itself is a statefulwidget and has Positioned widget inside
//also if we have multiple widgets of same type give them uniqueKey
AnimatedPositioned widget =
tester.firstWidget(find.byType(AnimatedPositioned));
double topPosition = widget.top;
print(widget);
print("AnimatedPositioned topPosition: $topPosition}");
expect(
screenRect.bottom > topPosition && screenRect.top < topPosition, true,
reason: 'should be on-screen');
//click button to animate the widget and wait
await tester.tap(find.byKey(fabKey));
//this will wait for animation to settle or call pump after duration
await tester.pumpAndSettle(const Duration(milliseconds: 300));
//check after position of the widget
AnimatedPositioned afterAnimationWidget =
tester.firstWidget(find.byType(AnimatedPositioned));
double afterAnimationTopPosition = afterAnimationWidget.top;
Rect animatedWidgetRect = tester.getRect(find.byType(AnimatedPositioned));
print("rect of widget : $animatedWidgetRect");
expect(
screenRect.bottom > afterAnimationTopPosition &&
screenRect.top < afterAnimationTopPosition,
false,
reason: 'should be off-screen');
});
Note: replaced _ from code as it was hiding the object from test file.
Output:
screenRect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
fab clicked
rect of widget : Rect.fromLTRB(0.0, -600.0, 24.0, -576.0)
I found this answer (in particular the code inside the onNotification), which kind of does what (I think) you want. It finds the RenderObject using the current context of the key. Afterwards it finds the RenderAbstractViewport using this RenderObject, and checks the offSetToReveal. Using this offset you can determine whether the current RenderObject is being displayed or not (using a simple comparison).
I'm not a 100% sure this will work / is what you want, but hopefully it can push you in the right direction.
Also (even though you stated you didn't want any external package), on the same question someone recommended this package, which can be useful for others having the same problem but who are open to using an external package.
I want to thank #parth-dave
for his answer, that I happily reward with the bounty
and Miyoyo referenced in the question
I want to offer my own implementation built on his approach
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
// !! uncomment tge line below to run as test app
// void main() => runApp(_myApp);
class Keys {
static final subject = UniqueKey();
static final parent = UniqueKey();
static final trigger = UniqueKey();
}
final _onScreen = ValueNotifier<bool>(true);
Widget get app => MaterialApp(
home: Scaffold(
key: Keys.parent,
body: MyStage(),
floatingActionButton: MyFAB(),
),
);
class MyFAB extends StatelessWidget {
const MyFAB() : super(key: const ValueKey('MyFAB'));
#override
Widget build(BuildContext context) => FloatingActionButton(
key: Keys.trigger,
onPressed: () => _onScreen.value = !_onScreen.value,
);
}
class MyStage extends StatelessWidget {
const MyStage() : super(key: const ValueKey('MyStage'));
#override
Widget build(BuildContext context) => Stack(
children: [
ValueListenableBuilder(
child: FlutterLogo(
key: Keys.subject,
),
valueListenable: _onScreen,
builder: (context, isOnStage, child) => AnimatedPositioned(
top: MediaQuery.of(context).size.height *
(_onScreen.value ? .5 : -1),
child: child,
duration: const Duration(milliseconds: 100),
),
),
],
);
}
void main() {
group('`AnimatedPositined` test', () {
testWidgets(
'WHEN no interaction with `trigger` THEN `subject` is ON SCREEN',
(tester) async {
await tester.pumpWidget(app);
final parent = tester.getRect(find.byKey(Keys.parent));
final subject = tester.getRect(find.byKey(Keys.subject));
expect(parent.overlaps(subject), true, reason: 'should be ON-screen');
});
testWidgets('WHEN `trigger` tapped THEN `subject` is OFF SCREEN`',
(tester) async {
await tester.pumpWidget(app);
await tester.tap(find.byKey(Keys.trigger));
await tester.pumpAndSettle(const Duration(milliseconds: 300));
final parent = tester.getRect(find.byKey(Keys.parent));
final subject = tester.getRect(find.byKey(Keys.subject));
expect(parent.overlaps(subject), false, reason: 'should be OFF-screen');
});
});
}