Flutter Testing: How to target (specific) widgets - flutter

I am testing an application created by the mobile apps division.
Hardly any of the widgets have any keys or labels to distinguish themselves from each other.
I'm having a hard timing even targeting a single widget, let alone 2 similar widgets on the same page; example: 2 text field widgets: username, password.
Right now, the only test I have is this:
testWidgets('Empty Login Box', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
final emailText = find.text("EMAIL");
expect(emailText, findsOneWidget);
});
And even this doesn't work. Here's the response:
00:40 +0: ...\EndevStudios\MedicalApp\gshDevWork\medical-app-frontend\integration_test\mock_image_upload_test.dart I00:43 +0: ...\EndevStudios\MedicalApp\gshDevWork\medical-app-frontend\integration_test\mock_image_upload_test.d 2,854ms
00:47 +0: Login Page Tests Empty Login Box
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "EMAIL" (ignoring offstage widgets)>
Which: means none were found but one was expected
When the exception was thrown, this was the stack:
#4 main.<anonymous closure>.<anonymous closure> (file:///D:/WEBDEV/EndevStudios/MedicalApp/gshDevWork/medical-app-frontend/integration_test/mock_image_upload_test.dart:29:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
This was caught by the test expectation on the following line:
file:///D:/WEBDEV/EndevStudios/MedicalApp/gshDevWork/medical-app-frontend/integration_test/mock_image_upload_test.dart line 29
The test description was:
Empty Login Box
════════════════════════════════════════════════════════════════════════════════════════════════════
00:47 +0 -1: Login Page Tests Empty Login Box [E]
Test failed. See exception logs above.
The test description was: Empty Login Box
00:48 +0 -1: Some tests failed.
I've been trying to use these CommonFinders class, but I can't seem to utilize them effectively.
https://docs.flutter.dev/cookbook/testing/widget/finders
https://api.flutter.dev/flutter/flutter_driver/CommonFinders-class.html
https://api.flutter.dev/flutter/flutter_test/CommonFinders-class.html
To anyone who can, please help!

You need to first pump your widget, if you don't do that, the Finder is not going to find your widget and will error:
The following TestFailure was thrown running a test: Expected: exactly
one matching node in the widget tree Actual: _TextFinder:<zero
widgets with text "EMAIL" (ignoring offstage widgets)> Which: means
none were found but one was expected
Try the following code:
testWidgets('Empty Login Box', (WidgetTester tester) async {
/// Pump your widget first:
await tester.pumpWidget(const LoginBoxWidget(
title: 'My Title',
message: 'Another parameter...',
)); // Parameters depend on your widget
final emailText = find.text("EMAIL");
expect(emailText, findsOneWidget);
});

Related

Flutter widget testing: Button tap won't work

I'm using widget testing in Flutter. I'm using the game template from here
https://github.com/flutter/samples/tree/main/game_template
and my test doesn't work as expected:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:escape_team/main.dart';
void main() {
testWidgets('Main menu can be started', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp(
inAppPurchaseController: null,
playerProgressPersistence: null,
settingsPersistence: null,
));
expect(find.text('PLAY'), findsOneWidget);
await tester.tap(find.text('PLAY'));
await tester.pumpAndSettle();
expect(find.text('MISSION SELECT'), findsOneWidget);
});
}
results in:
00:00 +0: Main menu can be started
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "MISSION SELECT" (ignoring offstage widgets)>
Which: means none were found but one was expected
However, when I manually tap the 'PLAY' button in my app, the "MISSION SELECT" screen appears. How can I find out what's going on - and wrong?
UPDATE:
I did some more research, I guess it's caused by a JSON that's being loaded by my app before the main menu is started.
missionLoader
.readJson('assets/json/missions_en.json')
.then((value) => GoRouter.of(context).go('/play'));
This calls
final String response = await rootBundle.loadString(jsonFile);
... and rootBundle file loading isn't supported out of the box in Flutter tests, as I see it?
I tried adding
WidgetsFlutterBinding.ensureInitialized();
to my app's main() method, as well as to the test's main method, but both did not help. Any further leads would be greatly appreciated! Thank you!

flutter `tester.scrollUntilVisible` throws "Bad state: Too many elements" exception

I have the following elements encapsulated into a single ListView in my material app:
home: Scaffold(
appBar: AppBar(title: const Text("Flutter Layout")),
body: ListView(children: [
fibonacciSection,
// a ListView supports app body scrolling when the app is run on a small device.
Image.asset("images/lake.jpg",
width: 600,
height: 240,
fit: BoxFit
.cover), // BoxFit.cover tells the framework that the image should be as small as possible but cover its entire render box.
titleSection,
buttonsSection,
textSection,
statesSection
])));
And when I run the unit tests which contain the following code snippet:
await tester.pumpWidget(const MyApp(key: Key("StateManagemetTests")));
final listFinder = find.byType(Scrollable);
final itemFinder = find.byType(TapboxB);
// Scroll until the item to be found appears.
await tester.scrollUntilVisible(
itemFinder,
500.0,
scrollable: listFinder,
);
It throws the following exception:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following StateError was thrown running a test:
Bad state: Too many elements
When the exception was thrown, this was the stack:
#0 Iterable.single (dart:core/iterable.dart:656:24)
#1 WidgetController.widget (package:flutter_test/src/controller.dart:69:30)
#2 WidgetController.scrollUntilVisible.<anonymous closure> (package:flutter_test/src/controller.dart:1190:15)
#3 WidgetController.scrollUntilVisible.<anonymous closure> (package:flutter_test/src/controller.dart:1188:39)
#6 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#7 WidgetController.scrollUntilVisible (package:flutter_test/src/controller.dart:1188:27)
#8 main.<anonymous closure> (file:///usr/src/flutter/flutter_app_layout/test/widget_test.dart:50:18)
<asynchronous suspension>
<asynchronous suspension>
(elided 3 frames from dart:async and package:stack_trace)
Any advice and insight is appreciated!
The error seems to occur when there are more than one Scrollable in the widget tree. Then Flutter doesn't know which one to scroll. You can solve that by first finding the correct Scrollable and telling scrollUntilVisible to use it:
// Scroll Save button into view
final listFinder = find.byType(Scrollable).last; // take last because the tab bar up top is also a Scrollable
expect(listFinder, findsOneWidget);
await tester.scrollUntilVisible(acceptButtonFinder, 100, scrollable: listFinder);
Enjoy!
Replacing scrollUntilVisible() with dragUntilVisible() solves the problem!
I don't find anything at all for scrollUntilVisible(). Is that an outdated API which should be removed from the framework?

setState() or markNeedsBuild() called during build inside a StreamBuilder

I am using this StreamBuilder to get the current location:
StreamBuilder<UserLocation>(
stream: locationService.locationStream,
builder: (context, snapshot) {
if (snapshot.data != null) {
bool es_actual = ubicacionesProvider.ubicacionActualSeleccionada;
bool es_elegida = ubicacionesProvider.ubicacionElegidaSeleccionada;
if(es_actual){
latitudData = snapshot.data.latitude;
// ubicacionesProvider.setlatitudActual(latitudData);
longitudData = snapshot.data.longitude;
//ubicacionesProvider.setlongitudActual(longitudData);
Coordinates misCoordenadas =
new Coordinates(latitudData, longitudData);
// ubicacionesProvider.setubicacionActual(_miDireccionActual);
getAddress(misCoordenadas);
}
if(es_elegida){
_latitudElegida = ubicacionesProvider.latitudElegida;
_longitudElegida = ubicacionesProvider.longitudElegida;
_miDireccionActual = ubicacionesProvider.ubicacionElegida;
}
}
I want to update a provider called ubicacionesProvider with some changes:
ubicacionesProvider.setlatitudActual(latitudData)
ubicacionesProvider.setlongitudActual(longitudData)
ubicacionesProvider.setubicacionActual(_miDireccionActual)
But I am getting a warning using one or all of them, the app is not exiting but the warning is shown:
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for UbicacionesProvider:
setState() or markNeedsBuild() called during build.
This _InheritedProviderScope<UbicacionesProvider> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<UbicacionesProvider>
value: Instance of 'UbicacionesProvider'
listening to value
The widget which was currently being built when the offending call was made was: StreamBuilder<UserLocation>
dirty
dependencies: [MediaQuery]
state: _StreamBuilderBaseState<UserLocation, AsyncSnapshot<UserLocation>>#39568
When the exception was thrown, this was the stack:
#0 Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4292:11)
#1 Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4307:6)
#2 _InheritedProviderScopeElement.markNeedsNotifyDependents (package:provider/src/inherited_provider.dart:496:5)
#3 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:226:25)
#4 UbicacionesProvider.setlatitudActual (package:flutter_qplan/providers/ubicaciones_provider.dart:50:5)
...
The UbicacionesProvider sending notification was: Instance of 'UbicacionesProvider'
====================================================================================================
I would like to update the provider without getting that warning.
Usually this happens when you setState or notifyListeners before the build has finished building all the widgets. Maybe you can add your update logic like this :
WidgetsBinding.instance!.addPostFrameCallback((_) {
// Add Your Update Code here.
});

Can't find widget by key even if it is in the tree

What are the possible reasons why a widget isn't found by key in a test, even if it's in the widget tree?
So in my app, I have some categories of websites and each category has a + button so users can add their own. Now, I want to test that the page opens correctly when the + button is pressed. If I just do:
await tester.tap(find.byIcon(Icons.add));
The test fails because it finds too many widgets and doesn't know which one to tap. So I figured I'd give keys to my + buttons by wrapping them in a container like this:
Container(
key: ValueKey('add_website_' + category.toLowerCase()),
child: AddWebsiteButton(), // the tappable widget that contains an Icon(Icons.add)
)
Now in my test, I call:
expect(find.byKey(ValueKey('add_website_learning')), findsOneWidget);
And, lo and behold, it fails:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _KeyFinder:<zero widgets with key [<'add_website_learning'>] (ignoring offstage widgets)>
Which: means none were found but one was expected
If I call debugDumpApp() just before the call above, I can see that the Container with that key is actually in the widget tree:
...
└KeyedSubtree-[Key <[<add_website_learning>]>]
└AutomaticKeepAlive(state: _AutomaticKeepAliveState#470b4(handles: no notifications ever received))
└KeepAlive(keepAlive: false)
└NotificationListener<KeepAliveNotification>
└IndexedSemantics(index: 4, renderObject: RenderIndexedSemantics#02bbd relayoutBoundary=up3 NEEDS-PAINT)
└RepaintBoundary(renderObject: RenderRepaintBoundary#8170e relayoutBoundary=up4 NEEDS-PAINT)
└Container-[<add_website_learning>]
└Tooltip("Add website", dependencies: [_LocalizationsScope-[GlobalKey#55b7a], _InheritedTheme, TickerMode-[<CrossFadeState.showSecond>]], state: _TooltipState#87315(ticker inactive))
└GestureDetector(startBehavior: start)
...
I tried making sure it's in view by calling ensureVisible, but that also fails saying it can't find the widget.
What am I doing wrong?

How to add album arts in flutter

I am using Flute music player plugin to make music player app in Flutter. But I am having a trouble for adding album art.
I wrote:
dynamic getImage(int idx) {
return _songs[idx].albumArt == null
? null
: new File.fromUri(Uri.parse(_songs[idx].albumArt));
}
And I used Image.file Widget:
Container(
childe: Image.file(getImage(_index))
)
And the result is:
I/flutter (15576): The following assertion was thrown building HyperMusicHome(dirty, state:
I/flutter (15576): _HyperMusicHomeState#b83f2):
I/flutter (15576): 'package:flutter/src/painting/image_provider.dart': Failed assertion: line 621 pos 14: 'file !=
I/flutter (15576): null': is not true.
I/flutter (15576):
I/flutter (15576): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter (15576): more information in this error message to help you determine and fix the underlying cause.
I/flutter (15576): In either case, please report this assertion by filing a bug on GitHub:
I/flutter (15576): https://github.com/flutter/flutter/issues/new?template=BUG.md
The error you're getting comes from this line in Flutter's FileImage class which checks whether what you're passing to Image.file() is null. It seems like the song you're currently viewing does not have an album art. All you need to do is not show the image when no album art is available.
I don't know what exactly your widget looks like, but you could do something like this:
#override
Widget build(BuildContext context) {
// Call `getImage` once to get the image file (which might be `null`)
final albumArtFile = getImage(_index);
return Container(
// Only show the image if `albumArtFile` is not `null`
child: albumArtFile != null ? Image.file(albumArtFile) : null,
);
}
You could also bundle a placeholder image with your app and show that when no album art is available:
albumArtFile != null ? Image.file(albumArtFile) : Image.asset('assets/placeholder.png')
You can learn more about adding assets to your app here.