I am having trouble testing my Riverpod FutureProvider when it returns an AsyncValue.error.
I tried the test as shown below:
// Future provider
final ipsProvider = FutureProvider.autoDispose((_) => IpRepository().fetchIps());
// Widget to be tested
class ExampleWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
// hooks
AsyncValue<List<Ip>> ips = watch(ipsProvider);
return Container(
child: ips.when(
data: (data) => randomWidget,
loading: () => progressIndicator,
error: (_, stack) => Text('YesMan'),
),
);
}
}
// Test: I am trying to find a Text widget containing message 'YesMan'
testWidgets('ExampleWidget | error', (WidgetTester tester) async
await tester.pumpWidget(
ProviderScope(
overrides: [
ipsProvider.overrideWithValue(AsyncValue.error('randomErrorMessage')),
],
child: MaterialApp(
home: Builder(builder: (context) {
return ExampleWidget();
}),
),
),
);
final finderError = find.text('YesMan');
expect(finderError, findsOneWidget);
});
I expected the test to return a text widget with message 'randomError', but instead it throws an exception as below:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK The following message was thrown running a test: randomErrorMessage When the exception was thrown, this was the stack:
Any idea on how to test AsyncValue.error cases in Riverpod?
Thank you
instead of overrideWithValue try overrideProvider and run a future.error so FutureProvider catch it
class ExampleWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
// hooks
AsyncValue<String> ips = watch(ipsProvider);
return Container(
child: ips.when(
data: (data) => SizedBox(),
loading: () => CircularProgressIndicator(),
error: (_, stack) => Text('YesMan'),
),
);
}
}
testWidgets('ExampleWidget | error', (WidgetTester tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
ipsProvider.overrideWithProvider(FutureProvider((_) => Future.error('error'))),
],
child: MaterialApp(
home: Builder(builder: (context) {
return ExampleWidget();
}),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pump();
final finderError = find.text('YesMan');
expect(finderError, findsOneWidget);
});
Related
I am trying to implement a logout page. So when the user clicks on logout button in the navigation following code is called:
Class Logout extends StatelessWidget {
#override
Widget build(BuildContext context) {
final provider = Provider.of<SignInProvider>(context, listen: true);
Future.delayed(Duration(seconds: 5), () async {
provider.isLoggedIn = false;
provider.notifyListeners();
Navigator.pushReplacement(
context, new MaterialPageRoute(builder: (context) => LoginGate()));
});
return Center(child: CircularProgressIndicator());
}
}
I get the following error:
The following assertion was thrown building MainScreen(dirty, dependencies: [_InheritedProviderScope<SelectedIndex?>, _InheritedProviderScope<SignInProvider?>], state: _MainScreenState#6a8ce):
setState() or markNeedsBuild() called during build.
I tried adding the delay hoping that would fix the issue but didn't help. Would appreciate some help on how to handle this.
Logout Button is shown using NavigationRail
const NavigationRailDestination(
icon: Icon(Icons.logout),
label: Text('Logout'),
),
And the Logout widget is called using following:
child: Row(
children: [
NavigationRailExample(),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: screenSwitch[providerSelectedIndex.selectedIndex],
)
],
),
List<Widget> screenSwitch = [
HomeScreen(),
Screen1(),
Screen2(),
Screen3(),
Screen4(),
Screen5(),
Screen6(),
Logout(),
];
You are calling you async function in build method which is wrong. Try this:
class Logout extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doLogOut(context),
builder: (context, snapshot) {
return Center(child: CircularProgressIndicator());
},
);
}
Future<void> doLogOut(BuildContext context) async {
final provider = Provider.of<SignInProvider>(context, listen: true);
await Future.delayed(Duration(seconds: 5), () async {
provider.isLoggedIn = false;
provider.notifyListeners();
Navigator.pushReplacement(
context, new MaterialPageRoute(builder: (context) => LoginGate()));
});
}
}
I'm passing the CartBloc instance to the BlocProvider. But the test is failing as the ListView widget is not found.
Below is the test code.
void main() {
late MockCartRepository cartRepository;
late MockSharedPreferenceService sharedPreferenceService;
late AuthBloc authBloc;
late MockUserRepository userRepository;
late CartData cartData;
late CartBloc cartBloc;
setUp(() {
cartData = CartData.fromJson(guestCartData);
cartRepository = MockCartRepository();
sharedPreferenceService = MockSharedPreferenceService();
userRepository = MockUserRepository();
authBloc = AuthBloc(
service: sharedPreferenceService,
userRepository: userRepository,
);
cartBloc = CartBloc(
cartRepository: cartRepository,
sharedPreferenceService: sharedPreferenceService,
authBloc: authBloc,
);
});
_pumpTestWidgetWithEvent(WidgetTester tester, CartEvent event) {
return MaterialApp(
home: BlocProvider(
create: (_) => cartBloc..add(event),
child: const CartView(),
),
);
}
group('Testing cart view', () {
testWidgets(
'Adding item to cart',
(WidgetTester tester) async {
when(cartBloc.cartRepository.addToCart(
cartId: cartData.id!,
quantity: 1,
variantId: cartData.lineItems!.physicalItems!.first.variantId!,
productId: cartData.lineItems!.physicalItems!.first.productId!,
)).thenAnswer((_) async => cartData);
when(sharedPreferenceService.setString('cartId', cartData.id!))
.thenAnswer((_) async => true);
await tester.pumpWidget(
_pumpTestWidgetWithEvent(
tester,
AddToCartEvent(
cartId: cartData.id!,
quantity: 1,
variantId: cartData.lineItems!.physicalItems!.first.variantId!,
productId: cartData.lineItems!.physicalItems!.first.productId!,
),
),
);
await tester.pumpAndSettle();
expect(find.text('Cart'), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
},
);
});
}
The test passes when I pass a new instance of cart bloc as shown below.
_pumpTestWidgetWithEvent(WidgetTester tester, CartEvent event) {
return MaterialApp(
home: BlocProvider(
create: (_) => CartBloc(
cartRepository: cartRepository,
sharedPreferenceService: sharedPreferenceService,
authBloc: authBloc,
)..add(event),
child: const CartView(),
),
);
}
Why is this happening?
I've also tried using BlocProvider.value and passing cartBloc as value. But this also failing.
Flutter no longer shows any error message, I'm using android studio, but even if I start the program in console messages still won't appear. For example if mapping an object goes wrong, there will be no error shown in console, I'll have to find it my self
This is my main file:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
ErrorWidget.builder = (FlutterErrorDetails details) => Container(
color: Colors.white,
child: const Center(
child: Text('Error'),
),
);
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} catch (e) {}
setupLocator();
await SentryFlutter.init((SentryFlutterOptions options) {
options.reportPackages = false;
options.enableOutOfMemoryTracking = true;
options.enableAppLifecycleBreadcrumbs = false;
options.anrEnabled = true;
options.debug = true;
options.dsn ='';
options.tracesSampleRate = 1.0;
}, appRunner: () => runApp(MyApp(route: route,)));
}
class MyApp extends StatelessWidget {
final String route;
final bool isLoggedIn;
MyApp({
required this.route,
required this.isLoggedIn,
});
#override
Widget build(BuildContext context) {
return GlobalBlocProviders(
isLoggedIn: isLoggedIn,
child: BlocListener<NotificationsBloc, NotificationsState>(
listener: (context, state) {
final route = state.route;
if (route == null) return;
locator<NavigationService>().navigateTo(route);
},
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: TylerTheme,
builder: (BuildContext context, Widget? childWidget) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat: true,
),
child: childWidget!,
);
},
initialRoute: route,
navigatorObservers: [
StackedService.routeObserver,
SentryNavigatorObserver()
],
navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute,
),
),
);
}
}
Would be perfect if you have any suggestions. Thank you!
You just have to print the error via print method.
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} catch (e) {
print("Catch Exception is $e");
}
I am trying to Widget test my WelcomeScreen(). WelcomeScreen has a BlocProvider and a BlocBuilder. After I load WelcomeBloc() it checks with an if statement inside the builder to check if the state is WelcomeLoadSuccessState.
How do I find something under the if statement if the statement is true?
My Welcome screen:
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WelcomeBloc(),
child: BlocBuilder<WelcomeBloc, WelcomeState>(
builder: (context, state) {
if (state is WelcomeLoadSuccessState) {
return Scaffold(
body: Container(
child: Column(
children: [
Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(8),
child: ShowUp(
delay: _delay + 200,
child: Text('Welcome user’, // <——— I want to find this one
)),
),
],
),
],
)),
);
}
// return LoadingWidget();
return Text('Something'); // <——— This one I can find
},
),
);
}
The test that I have now:
main() {
WelcomeBloc welcomeBloc;
WelcomeService welcomeService;
final Brand brand = Brand();
setUp(() {
setUpMocks();
welcomeService = localServices<WelcomeService>();
welcomeBloc = MockWelcomeBloc();
});
_createWidget(WidgetTester tester) async {
when(welcomeService.getBrand(id: '609a88d324a01928242d1ca9')).thenAnswer((realInvocation) => Future.value(brand));
welcomeBloc.add(WelcomeLoadRequestEvent(id: '609a88d324a01928242d1ca9'));
when(welcomeBloc.state).thenAnswer((_) => WelcomeLoadSuccessState(brand: brand));
print(welcomeBloc.state); //Correct State (WelcomeLoadSuccessState)
await tester.pumpWidget(
MaterialApp(
title: 'Flutter Demo',
home: WelcomeScreen(),
)
);
await tester.pump();
}
testWidgets('Welcome Screen Test', (WidgetTester tester) async {
await _createWidget(tester);
await tester.pump();
//expect(find.textContaining('Welcome user'), findsOneWidget); //What I want
expect(find.text('Something'), findsOneWidget); //This works
});
tearDown(() {
welcomeBloc?.close();
});
}
Thank you for helping.
I solved it:
change:
create: (context) => WelcomeBloc()
to:
create: (context) => WelcomeBloc()..add(WelcomeLoadRequestEvent(id: '609a88d324a01928242d1ca9')),
and my test is now this:
main() {
WelcomeBloc welcomeBloc;
WelcomeService welcomeService;
final Brand brand = Brand();
setUp(() {
setUpMocks();
welcomeService = localServices<WelcomeService>();
welcomeBloc = MockWelcomeBloc();
});
_createWidget(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
title: 'Flutter Demo',
home: WelcomeScreen(),
));
await tester.pump(Duration(seconds: 10));
}
testWidgets('Welcome Screen Test', (WidgetTester tester) async {
when(welcomeService.getBrand(id: '609a88d324a01928242d1ca9'))
.thenAnswer((realInvocation) => Future.value(brand));
whenListen(
welcomeBloc,
Stream.fromIterable([
WelcomeLoadInProgressState(),
WelcomeLoadSuccessState(brand: brand),
]));
await _createWidget(tester);
await tester.pump(Duration(seconds: 5));
expect(find.textContaining('Welcome user'), findsOneWidget);
});
tearDown(() {
welcomeBloc?.close();
unRegister();
});
}
Edit to add:
For my other pages it was useful to separate the blocProvider and the blocBuilder. This way I was able to Mock my blocProvider with a MockMyBloc() and then give the screen in the child.
My real widgets:
MyWidgetMakeBlocProviders(
Widget build(context) {
return BlocProvider<MyBloc>(
create: (context) => MyBloc(),
child: MyScreen(),
);
}
)
MyScreen(
Widget build(context) {
return BlocBuilder<MyBloc, MyBlocState>(
builder: (context, state) {...}
);
}
)
My test:
testWidgets('', (tester) async {
whenListen(MockMyBloc, Stream.fromIterable([
InitState(),
LoadedState()
]));
await _createWidget(tester);
await tester.pump();
//expect()
});
_createWidget(tester) async {
await tester.pumpWidget(
MaterialApp(
title: '',
home: BlocProvider<MockMyBloc>(
create: (context) => MockMyBloc(),
child: MyScreen(),
)
)
);
await tester.pump();
}
I have create a Alert Dialog for OTP Verification after verifying OTP I close it and then I had created a another dialog which is for data processing... and then I close it.
Result:-
First OTP Dialog closed after OTP verification by calling Navigator.of(context).pop(); and then second dialog just pops up but It does not closed after calling Navigator.of(context).pop();
What I want to do:
Close OTP Dialog after verifying OTP (Works)
Open Progress dialog (Works)
Close it after uploading profile in firebase storage (Does not Works)
Please help me solve this issue.
Thanks in Advance !
You probably forgetting await somewhere in your code.
Try this,
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController _otpCtrl = TextEditingController();
void dispose() {
_otpCtrl.dispose();
super.dispose();
}
Future<void> _verifyOTP() async {
final String otp = await _inputOtp();
String otpValidationError;
if (otp != null) otpValidationError = await _sendOtpVerifyRequest();
print(otpValidationError);
}
Future<String> _sendOtpVerifyRequest() async {
showDialog(
context: context,
builder: (context) {
return Center(child: CircularProgressIndicator());
},
);
await Future.delayed(Duration(seconds: 2)); //TODO: Do post request here
Navigator.pop(context);
return null;
}
Future<String> _inputOtp() async {
final flag = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Enter OTP"),
content: TextField(
controller: _otpCtrl,
decoration: InputDecoration(
hintText: "x x x x x x",
),
),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context, false);
},
),
FlatButton(
child: Text("Confirm"),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
},
);
if (flag == true)
return _otpCtrl.text;
else
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: _verifyOTP,
child: Text("Click Here"),
),
),
);
}
}