is there a way to test the content within a show dialog?
I am trying to do BDD in the project, The following is the scenario:
As a User, I would like to add a photo or select one from the gallery so that I can use it on the item.
The following is the code I am using to test it but for some reason, the test fails.
add_item_view.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:my_app_mobile/models/graded_item/graded_item.dart';
import 'package:my_app_mobile/template/index.dart' as template;
import 'package:image_picker/image_picker.dart';
class AddItemView extends HookWidget {
final GradedItem gradedItem;
static final Key photoKey = Key('#photoKey');
static final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final void Function() onPhoto;
final ImagePicker _imagePicker = ImagePicker();
AddItemView({
#required this.gradedItem,
this.onPhoto,
});
#override
Widget build(BuildContext context) {
final _image = useState<File>();
Future getImage() async {
final pickedFile = await _imagePicker.getImage(source: ImageSource.camera);
if (pickedFile != null) {
_image.value = File(pickedFile.path);
} else {
print('No image selected.');
}
}
return Scaffold(
appBar: AppBar(),
body: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
children: [
GestureDetector(
onTap: () async {
if (onPhoto != null) {
onPhoto();
}
showDialog(
context: context,
barrierColor: Colors.red.withOpacity(.2),
builder: (context) {
return CameraOptions();
},
);
},
child: Container(
key: photoKey,
alignment: Alignment.center,
child: Icon(
Icons.add_a_photo,
color: Theme.of(context).primaryColor,
size: 44.0,
),
height: 100.0,
width: 100.0,
decoration: BoxDecoration(
color: template.colors.grey340,
borderRadius: BorderRadius.circular(10.0),
),
),
),
],
),
),
),
);
}
}
class CameraOptions extends StatelessWidget {
static final Key captureButtonPhotoKey = Key('#captureButtonPhotoKey');
static final Key chooseButtonPhotoKey = Key('#chooseButtonPhotoKey');
static final Key cancelButtonKey = Key('#cancelButtonKey');
final void Function() onCapture;
final void Function() onChoose;
final void Function() onCancel;
CameraOptions({this.onCapture, this.onChoose, this.onCancel});
#override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 200.0,
width: 200.0,
child: Icon(Icons.camera, color: Colors.blue),
),
);
}
}
add_item_view_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'my_app_mobile/models/graded_item/graded_item.dart';
import 'my_app_mobile/views/dashboard/children/collection/children/add_item/add_item.dart';
import 'package:mockito/mockito.dart';
void main() {
Widget mountApp({GradedItem gradedItem, void Function() onPhoto}) {
return MaterialApp(
home: AddItemView(
gradedItem: gradedItem,
onPhoto: onPhoto,
),
);
}
testWidgets('should build with no problems', (tester) async {
await tester.pumpWidget(mountApp(gradedItem: GradedItem.generate()));
expect(find.byType(AddItemView), findsOneWidget);
});
group('photo', () {
testWidgets(
'should photo widget be available',
(tester) async {
await tester.pumpWidget(mountApp(
gradedItem: GradedItem.generate(),
));
expect(find.byKey(AddItemView.photoKey), findsOneWidget);
},
);
testWidgets(
'should be able to call onPhoto handler when is available',
(tester) async {
final fn = MockedFunctions();
await tester.pumpWidget(mountApp(
gradedItem: GradedItem.generate(),
onPhoto: fn.onPhoto,
));
expect(find.byKey(AddItemView.photoKey), findsOneWidget);
await tester.tap(find.byKey(AddItemView.photoKey));
verify(fn.onPhoto()).called(1);
},
);
testWidgets(
'should onPhoto handler open camera options',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return AddItemView(
gradedItem: GradedItem.generate(),
// onPhoto: fn.onPhoto,
);
},
),
),
),
);
await tester.tap(find.byKey(AddItemView.photoKey));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byIcon(Icons.camera), findsOneWidget);
},
);
});
}
class MockedFunctions extends Mock {
void onPhoto();
}
Is there a way to do testing on a showDialog function?
Solved, for some reason, the code I posted is working, I restarted my computer and now they are working.
Related
I'm using sdk 2.12.0 and image_picker 0.8.4 version.
I'm going to link my gallery to get an image.
However, when I press the Add Image button on my app, the app turns off immediately.
This is the code for the image_picker I used.
class CreatePage extends StatefulWidget {
const CreatePage({Key? key, required this.user}) : super(key: key);
final User user;
#override
_CreatePageState createState() => _CreatePageState();
}
class _CreatePageState extends State<CreatePage> {
//ImagePicker
final ImagePicker _picker = ImagePicker();
File? _imageFile;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppbar(),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _getImage,
backgroundColor: Colors.blue,
child: Icon(Icons.add_a_photo),
),
);
}
Future<void> _getImage() async {
//ImagePiker
var image = await _picker.pickImage(source: ImageSource.gallery);
setState(() {
_imageFile = File(image!.path);
});
}
And this is my full code about this page. (Firebase code is included)
import 'dart:io';
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class CreatePage extends StatefulWidget {
//user info
const CreatePage({Key? key, required this.user}) : super(key: key);
final User user;
#override
_CreatePageState createState() => _CreatePageState();
}
class _CreatePageState extends State<CreatePage> {
//input text
final TextEditingController createText = TextEditingController();
//ImagePicker
final ImagePicker _picker = ImagePicker();
File? _imageFile;
//_createPageState가 제거될 때 호출됨
#override
void dispose() {
// TODO: implement dispose
createText.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppbar(),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _getImage,
backgroundColor: Colors.blue,
child: Icon(Icons.add_a_photo),
),
);
}
_buildAppbar() {
return AppBar(
actions: [
IconButton(
icon: Icon(Icons.send),
onPressed: () {
_uploadPost(context);
},
),
],
);
}
_buildBody() {
return SingleChildScrollView(
child: Column(
children: [
_imageFile == null ? Text("No Image") : Image.file(_imageFile!),
TextField(
controller: createText,
decoration: InputDecoration(
hintText: "내용을 입력하세요",
),
)
],
),
);
}
//gallery image
Future<void> _getImage() async {
var image = await _picker.pickImage(source: ImageSource.gallery);
setState(() {
_imageFile = File(image!.path);
});
}
Future _uploadPost(BuildContext context) async {
final firebaseStorageRef = FirebaseStorage.instance
.ref()
.child('post')
.child('${DateTime.now().microsecondsSinceEpoch}.png');
final task = await firebaseStorageRef.putFile(
_imageFile!, SettableMetadata(contentType: "image/png")
);
final uri = await task.ref.getDownloadURL();
//database document
final doc = FirebaseFirestore.instance.collection('post').doc();
//json type
await doc.set({
'id': doc.id,
'photoUrl': uri.toString(), //storage file url
'contents': createText, //user input text
'email': widget.user.email, //user email
'displayName': widget.user.displayName, //user name
'userPhotoUrl': widget.user.photoURL, //user profile image
});
//return page
Navigator.pop(context);
}
}
Pressing the floatingActionButton turns off the app and outputs the following error.
Lost connection to device.
May I know the cause of this?
Thank you in advance.
try adding dependency
image_picker: ^0.8.3+2
import 'package:image_picker/image_picker.dart';
then add this code
String url = "";
ImagePicker image = ImagePicker();
File ? file;
getImage() async {
var img = await image.pickImage(source: ImageSource.gallery);
setState(() {
file = File(img!.path);
});
}
And add:
onTap: () {
getImage();
},
add code:
child: file != null ?
Column(
children: [
Padding(
padding: const EdgeInsets.all(20.0),
child: Container(
color: Colors.black,
margin: EdgeInsets.only(top: 80),
width: double.infinity,
height: 250,
child: Image.file(
file!,
fit: BoxFit.contain,
),
),
),
My guess is that you don't have permission to access the Media on the device, thus aborting the app the moment you try to do so. Check out the permission handler package.
The test is to check that the showModelBottomSheet move along the keyboard or not, showModelBottomSheet must show above the keyboard.
I write this code but failed.
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/basic.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('To Check the Position of showModelBottomSheet on keyboard popup', () {
late BuildContext savedContext;
Widget _buildTest(MediaQueryData data, Key buttonKey, Key showModelBottomSheetKey){
return MaterialApp(
home: Builder(
builder: (BuildContext context) {
return FlatButton(
key: buttonKey,
onPressed: () {
print(MediaQuery.of(context).size);
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
savedContext = context;
return Container(
key: showModelBottomSheetKey,
child: const TextField(),
);
},
);
},
child: const Text('Press'),
);
},
),
);
}
Future<void> _runFloatTests(WidgetTester tester, {required Rect defaultRect}) async {
const double keyboardHeight = 200.0;
const double viewPadding = 50.0;
final GlobalKey buttonKey = GlobalKey();
final GlobalKey showModelBottomSheetKey = GlobalKey();
// Default without Keyboard
await tester.pumpWidget(_buildTest(
const MediaQueryData(
viewPadding: EdgeInsets.only(bottom: viewPadding)
),
buttonKey,
showModelBottomSheetKey));
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
print();
print(tester.getRect(find.byKey(showModelBottomSheetKey)));
print(rectMoreOrLessEquals(defaultRect));
print('-------------------');
expect(
tester.getRect(find.byKey(showModelBottomSheetKey)),
rectMoreOrLessEquals(defaultRect),
);
// Present keyboard and check position, should change
await tester.pumpWidget(_buildTest(
const MediaQueryData(
viewPadding: EdgeInsets.only(bottom: viewPadding),
viewInsets: EdgeInsets.only(bottom: keyboardHeight),
),
buttonKey,
showModelBottomSheetKey
));
print(tester.getRect(find.byKey(showModelBottomSheetKey)));
print(rectMoreOrLessEquals(defaultRect.translate(
0.0,
-keyboardHeight,
)));
print('-------------------');
expect(
tester.getRect(find.byKey(showModelBottomSheetKey)),
rectMoreOrLessEquals(defaultRect.translate(
0.0,
-keyboardHeight,
)),
);
}
testWidgets('',
(WidgetTester tester) async {
const Rect defaultRect = Rect.fromLTRB(0.0, 552.0, 800.0, 600.0);
await _runFloatTests(tester, defaultRect: defaultRect);
await tester.pump();
});
});
}
I was create SharedPreferences to save user loading in logon page. Then data of user will be save in SharedPreferences and move to main page. But my problem now in main page I need use this variable in different places in main page. But I cant do that.
I need to make variable of logindata can use in each places in main page I try to use in drawer to make logout. No I get error as:
Undefined name 'logindata'.
this is my code:
void initial() async {
logindata = await SharedPreferences.getInstance();
setState(() {
username = logindata.getString('username');
return username;
});
}
my full code:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'addnewtopics.dart';
import 'DetilesOfMainPage.dart';
import 'loginpage.dart';
class MyApp extends StatelessWidget {
final String email;
MyApp({Key key, #required this.email}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('JSON ListView')
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
logindata.setBool('login', true);// here I need to use It ========================
Navigator.pushReplacement(context,
new MaterialPageRoute(builder: (context) => LoginUser()));
Navigator.pop(context);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
// Navigator.pop(context);
},
),
],
),
),
body: JsonImageList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => UploadImageDemo()
),);
},
child: Icon(Icons.add),
),
));
}
}
class Flowerdata {
int id;
String flowerName;
String flowerImageURL;
Flowerdata({
this.id,
this.flowerName,
this.flowerImageURL
});
factory Flowerdata.fromJson(Map<String, dynamic> json) {
return Flowerdata(
id: json['id'],
flowerName: json['nametopics'],
flowerImageURL: json['image']
);
}
}
class JsonImageList extends StatefulWidget {
JsonImageListWidget createState() => JsonImageListWidget();
}
class JsonImageListWidget extends State {
SharedPreferences logindata;
String username;
#override
void initState() {
// TODO: implement initState
super.initState();
initial();
}
void initial() async {
logindata = await SharedPreferences.getInstance();
setState(() {
username = logindata.getString('username');
return username;
});
}
final String apiURL = 'http://xxxxxxxxx/getFlowersList.php';
Future<List<Flowerdata>> fetchFlowers() async {
var response = await http.get(apiURL);
if (response.statusCode == 200) {
final items = json.decode(response.body).cast<Map<String, dynamic>>();
List<Flowerdata> listOfFruits = items.map<Flowerdata>((json) {
return Flowerdata.fromJson(json);
}).toList();
return listOfFruits;
}
else {
throw Exception('Failed to load data from Server.');
}
}
getItemAndNavigate(String item, BuildContext context){
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(itemHolder : item)
)
);
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Flowerdata>>(
future: fetchFlowers(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Center(
child: CircularProgressIndicator()
);
return ListView(
children: snapshot.data
.map((data) => Column(children: <Widget>[
GestureDetector(
onTap: ()=>{
getItemAndNavigate(data.flowerName, context)
},
child: Row(
children: [
Container(
width: 200,
height: 100,
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child:
Image.network(data.flowerImageURL,
width: 200, height: 100, fit: BoxFit.cover,))),
Flexible(child:
Text(data.flowerName,
style: TextStyle(fontSize: 18)))
]),),
Divider(color: Colors.black),
],))
.toList(),
);
},
);
}
}
Anyone know how can make that?
You need var keyword, in your case you can directly use
var logindata = await SharedPreferences.getInstance();
You do not need to make it global, because SharedPreferences.getInstance() is Singleton
Every time you use var logindata = await SharedPreferences.getInstance(); will get the same instance
Also there is no performance issue when you call getInstance(), because it's cached, you can see source code snippet below
class SharedPreferences {
SharedPreferences._(this._preferenceCache);
...
static Future<SharedPreferences> getInstance() async {
if (_completer == null) {
_completer = Completer<SharedPreferences>();
try {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
_completer.complete(SharedPreferences._(preferencesMap));
} on Exception catch (e) {
// If there's an error, explicitly return the future with an error.
// then set the completer to null so we can retry.
_completer.completeError(e);
final Future<SharedPreferences> sharedPrefsFuture = _completer.future;
_completer = null;
return sharedPrefsFuture;
}
}
return _completer.future;
When you declare a String outside of class and does not contain _ before variable name like _localString it become global
String globalString = ""; //global, import can be seen
String _localString = ""; //local and can only be seen in this file, import can not seen
void main() async{
var logindata = await SharedPreferences.getInstance();
runApp(MyApp());
}
You simply need to put your variable outside of any class or method. An example is to create a file globals.dart then put all your globals in it and import the file when you need.
Example
// globals.dart
String globalString;
int globalInt;
bool globalBool;
// in any other file
import 'globals.dart' as globals;
globals.globalString = "Global String";
I am new to flutter and I am having the following problem.
I am trying to use the progressDialog in in a listview, I make the query to my database, I extract the list and pass it to the listview, I am trying to use the progressDialog so when I start loading the list it will run and tell the user to wait, and when I finish loading the list then the progressDialog is hidden, so far it works for me by bringing me the list and the progressDialog is executed saying to wait, but when I put the progressDialog.hide where the loading of the list ends I this is not accepting that line of code (progressDialog .hidde)
image:
enter image description here
import 'dart:convert';
import 'package:fluterproyecto1/Modulos/DetalleUser.dart';
import 'package:flutter/material.dart';
import 'package:progress_dialog/progress_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
String username2 = '';
String profesion = '';
String name = '';
class MemberPage extends StatefulWidget {
MemberPage({Key key}) : super(key: key);
#override
_MemberPageState createState() => _MemberPageState();
}
class _MemberPageState extends State<MemberPage> {
Map data;
List userData;
ProgressDialog progressDialog;
String name = '';
Future getData() async {
http.Response response =
await http.get("http://masciudad.com.co/flutter/getdata.php");
data = json.decode(response.body);
//setState(() {
//progressDialog.show();
userData = data["data"];
//progressDialog.hide();
//});
}
#override
void initState() {
super.initState();
getData();
}
#override
Widget build(BuildContext context) {
progressDialog = ProgressDialog(context, type: ProgressDialogType.Normal);
progressDialog.style(message: 'Por favor espere...');
progressDialog.show();
setState(() {
obtenerPreferencias();
});
return Scaffold(
appBar: AppBar(
title: Text("Bienvenido $username2"),
),
body: ListView.builder(
itemCount: userData == null ? 0 : userData.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
children: <Widget>[
Image.asset(
"assets/128.jpg",
width: 30.0,
height: 30.0,
),
//CircleAvatar(
///cuando la imagen es de interntet
//backgroundImage: NetworkImage(
// "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg"),
//),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"${userData[index]["username"]} - ${userData[index]["profesion"]}",
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w700,
),
),
)
],
),
),
onTap: () => Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new DetalleUser(name: userData[index]["username"]))),
);
},
),
//Navigator.of(context).push(MaterialPageRoute(
// builder: (BuildContext context) => MyHomePage()));
//Navigator.pushReplacementNamed(context, "/MyHomePage");
);
}
Future obtenerPreferencias() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
setState(() {
username2 = preferences.get("username2") ?? "";
profesion = preferences.get("profesion") ?? "";
});
}
Future destruirPreferencias() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.clear();
}
}
You can copy paste run full code below
You can use addPostFrameCallback and put logical in another function getRelatedData()
code snippet
void getRelatedData() async {
progressDialog = ProgressDialog(context, type: ProgressDialogType.Normal);
progressDialog.style(message: 'Por favor espere...');
progressDialog.show();
await getData();
await obtenerPreferencias();
progressDialog.hide();
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
getRelatedData();
});
}
working demo
full code
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:progress_dialog/progress_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
String username2 = '';
String profesion = '';
String name = '';
class MemberPage extends StatefulWidget {
MemberPage({Key key}) : super(key: key);
#override
_MemberPageState createState() => _MemberPageState();
}
class _MemberPageState extends State<MemberPage> {
Map data;
List userData;
ProgressDialog progressDialog;
String name = '';
Future getData() async {
http.Response response =
await http.get("http://masciudad.com.co/flutter/getdata.php");
data = json.decode(response.body);
//setState(() {
//progressDialog.show();
userData = data["data"];
print("getData Done");
//progressDialog.hide();
//});
}
void getRelatedData() async {
progressDialog = ProgressDialog(context, type: ProgressDialogType.Normal);
progressDialog.style(message: 'Por favor espere...');
progressDialog.show();
await getData();
await obtenerPreferencias();
progressDialog.hide();
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
getRelatedData();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Bienvenido $username2"),
),
body: ListView.builder(
itemCount: userData == null ? 0 : userData.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
children: <Widget>[
Image.network(
"https://picsum.photos/250?image=9",
width: 30.0,
height: 30.0,
),
//CircleAvatar(
///cuando la imagen es de interntet
//backgroundImage: NetworkImage(
// "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg"),
//),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"${userData[index]["username"]} - ${userData[index]["profesion"]}",
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w700,
),
),
)
],
),
),
onTap: () {
/*Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new DetalleUser(name: userData[index]["username"])));*/
});
},
),
//Navigator.of(context).push(MaterialPageRoute(
// builder: (BuildContext context) => MyHomePage()));
//Navigator.pushReplacementNamed(context, "/MyHomePage");
);
}
Future obtenerPreferencias() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
setState(() {
username2 = preferences.get("username2") ?? "";
profesion = preferences.get("profesion") ?? "";
});
}
Future destruirPreferencias() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.clear();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MemberPage(),
);
}
}
Dear #Yeimer I would suggest read documentation carefully
https://pub.dev/packages/progress_dialog
Add the Package this
dependencies:
progress_dialog: ^1.2.4
1
Initialize your ProgressDialog object
final ProgressDialog prDialog = ProgressDialog(context);
//For normal dialog
prDialog = ProgressDialog(context,type: ProgressDialogType.Normal, isDismissible: true/false, showLogs: true/false);
2 Showing the progress dialog
await pr.show();
3
Dismissing the progress dialog
prDialog.hide().then((isHidden) {
print(isHidden);
});
// or simply
await prDialog.hide();
I'm having a problem with the attached widget tests in flutter. When I run the tests individually, each of them succeeds; however, when I run the entire main() method, the first three tests succeed but the last two fail with the following exception:
Expected: exactly one matching node in the widget tree
Actual: ?:<zero widgets with type "SuccessDialog" (ignoring offstage widgets)>
I understand that the exception means that the widget I'm expecting is not present - what I don't understand is why the test succeeds when run individually but fails after being run after other tests. Is there some instance I need to be "resetting" after each test?
I've tried inserting "final SemanticsHandle handle = tester.ensureSemantics();" at the start of each tests and "handle.dispose();" at the end of each test but got the same results.
EDIT:
After some further investigating it seems like the problem may be with how I manage bloc instances using the flutter_bloc package. I have altered my tests to create a new testWidget instance for each test but am still encountering the same problem. Is there anything I may be missing that would cause a bloc instance to persist across testWidget objects?
My new test code looks like this:
main() {
MvnoMockClient.init();
testWidgets(
'Voucher Redemption: Tapping redeem when no values were entered yields 2 field errors',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsNWidgets(2));
});
testWidgets(
'Voucher Redemption: Tapping redeem when only voucher number was entered yields one field error',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsOneWidget);
});
testWidgets(
'Voucher Redemption: Tapping redeem when only mobile number was entered yields one field error',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsOneWidget);
});
testWidgets(
'Voucher Redemption: A successful server response yields a success dialog',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.text("REDEEM"));
await tester.pump();
expect(find.byType(SuccessDialog), findsOneWidget);
});
testWidgets(
'Voucher Redemption: An unsuccessful server response yields an error dialog',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
gToken = "invalid";
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
gToken = "validToken";
expect(find.byType(ErrorDialog), findsOneWidget);
});
}
For additional reference, I have also included the code for the VoucherRedemptionPage and VoucherRedemptionScreen below:
class VoucherRedemptionPage extends StatelessWidget {
final onSuccess;
final onFail;
const VoucherRedemptionPage({Key key, #required this.onSuccess, #required this.onFail})
: super(key: key);
#override
Widget build(BuildContext context) {
var _voucherRedemptionBloc = new VoucherRedemptionBloc();
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/" + gFlavor + "/primary_background.png"),
fit: BoxFit.cover),
),
child: new Scaffold(
backgroundColor: Colors.transparent,
appBar: new AppBar(
title: new Text(gDictionary.find("Redeem Voucher")),
),
body: new VoucherRedemptionScreen(
voucherRedemptionBloc: _voucherRedemptionBloc,
onSuccess: this.onSuccess,
onFail: this.onFail,
),
),
);
}
}
class VoucherRedemptionScreen extends StatefulWidget {
const VoucherRedemptionScreen({
Key key,
#required VoucherRedemptionBloc voucherRedemptionBloc,
#required this.onSuccess,
#required this.onFail,
}) : _voucherRedemptionBloc = voucherRedemptionBloc,
super(key: key);
final VoucherRedemptionBloc _voucherRedemptionBloc;
final onSuccess;
final onFail;
#override
VoucherRedemptionScreenState createState() {
return new VoucherRedemptionScreenState(
_voucherRedemptionBloc, onSuccess, onFail);
}
}
class VoucherRedemptionScreenState extends State<VoucherRedemptionScreen> {
final VoucherRedemptionBloc _voucherRedemptionBloc;
final onSuccess;
final onFail;
TextEditingController _msisdnController = TextEditingController();
TextEditingController _voucherPinController = TextEditingController();
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
VoucherRedemptionScreenState(
this._voucherRedemptionBloc, this.onSuccess, this.onFail);
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<VoucherRedemptionEvent, VoucherRedemptionState>(
bloc: _voucherRedemptionBloc,
builder: (
BuildContext context,
VoucherRedemptionState currentState,
) {
if (currentState is VoucherRedemptionInitial) {
_voucherPinController.text = currentState.scannedNumber;
return _buildFormCard();
}
if (currentState is VoucherRedemptionLoading) {
return Center(
child: CircularProgressIndicator(),
);
}
if (currentState is VoucherRedemptionSuccess) {
return SuccessDialog(
title: gDictionary.find("Voucher Redeemed Successfully"),
description: currentState.successMessage,
closeText: gDictionary.find("OK"),
closeAction: () {
this.onSuccess();
_voucherRedemptionBloc.dispatch(ResetVoucherRedemptionState());
},
);
}
if (currentState is VoucherRedemptionError) {
return ErrorDialog(
errorCode: currentState.errorCode,
errorMessage: currentState.errorMessage,
closeText: gDictionary.find("OK"),
closeAction: () {
this.onFail();
_voucherRedemptionBloc.dispatch(ResetVoucherRedemptionState());
},
);
}
},
);
}
Widget _buildFormCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(8))),
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
width: double.infinity,
height: double.infinity,
child: _buildCardContent(),
);
}
Widget _buildCardContent() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
gDictionary.find("Transaction Amount"),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).primaryColorDark,
fontWeight: FontWeight.bold),
),
Container(height: 16),
Form(
key: _formKey,
child: _buildFormContent(),
),
],
),
);
}
Column _buildFormContent() {
return Column(
children: <Widget>[
PlainTextField(
controller: _voucherPinController,
label: gDictionary.find("Voucher Number"),
required: true,
),
Container(height: 16),
MsisdnField(
controller: _msisdnController,
label: gDictionary.find("Mobile Number"),
required: true,
),
Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
SecondaryCardButton(
text: gDictionary.find("SCAN VOUCHER"),
onPressed: () {
_voucherRedemptionBloc.dispatch(
ScanBarcode(),
);
},
),
Container(
width: 8.0,
),
PrimaryCardButton(
text: gDictionary.find("REDEEM"),
onPressed: () {
if (_formKey.currentState.validate()) {
_voucherRedemptionBloc.dispatch(
RedeemVoucher(
_voucherPinController.text,
_msisdnController.text,
),
);
}
},
),
],
)
],
);
}
}
Found the problem. I was using the singleton pattern when creating an instance of the bloc - this caused states to persist across different widget objects. Very unlikely that anyone will encounter the same problem that I did but below is the code that I changed to mitigate the problem
Old problematic code:
class VoucherRedemptionBloc
extends Bloc<VoucherRedemptionEvent, VoucherRedemptionState> {
static final VoucherRedemptionBloc _voucherRedemptionBlocSingleton =
new VoucherRedemptionBloc._internal();
factory VoucherRedemptionBloc() {
return _voucherRedemptionBlocSingleton;
}
VoucherRedemptionBloc._internal();
//...
}
Updated working code:
class VoucherRedemptionBloc
extends Bloc<VoucherRedemptionEvent, VoucherRedemptionState> {
VoucherRedemptionBloc();
//...
}
That likely happens because your tests mutate some global variable but do not reset their value.
One way to make it safe is to always use setUp and tearDown instead of mutating variables directly the main scope:
int global = 0;
void main() {
final initialGlobalValue = global;
setUp(() {
global = 42;
});
tearDown(() {
global = initialGlobalValue;
});
test('do something with "global"', () {
expect(++global, 43);
});
test('do something with "global"', () {
// would fail without setUp/tearDown
expect(++global, 43);
});
}
Similarly, if a test needs to modify a variable, use addTearDown instead of manually resetting the value later in the test.
DON'T:
int global = 0;
test("don't", () {
global = 43;
expect(global, 43);
global = 0;
})
DO:
int global = 0;
test('do', () {
global = 43;
addTearDown(() => global = 0);
expect(global, 43);
});
This ensures that the value will always be reset even if the tests fails – so that other test functions normally.
In my case I wasn't setting skipOffstage: false, doing something like this worked for me:
expect(find.text('text', skipOffstage: false), findsNWidgets(2));