mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Handle network connectivity a bit different
When the server is not reachable, we show a slightly different error message and remove the option to automatically create an issue.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -31,41 +32,27 @@ import 'package:wger/main.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
|
||||
void showHttpExceptionErrorDialog(
|
||||
WgerHttpException exception,
|
||||
BuildContext context,
|
||||
) {
|
||||
void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? context}) {
|
||||
final logger = Logger('showHttpExceptionErrorDialog');
|
||||
|
||||
// Attempt to get the BuildContext from our global navigatorKey.
|
||||
// This allows us to show a dialog even if the error occurs outside
|
||||
// of a widget's build method.
|
||||
final BuildContext? dialogContext = context ?? navigatorKey.currentContext;
|
||||
|
||||
if (dialogContext == null) {
|
||||
if (kDebugMode) {
|
||||
logger.warning('Error: Could not error show http error dialog because the context is null.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.fine(exception.toString());
|
||||
|
||||
final List<Widget> errorList = [];
|
||||
|
||||
for (final key in exception.errors!.keys) {
|
||||
// Error headers
|
||||
// Ensure that the error heading first letter is capitalized.
|
||||
final String errorHeaderMsg = key[0].toUpperCase() + key.substring(1, key.length);
|
||||
|
||||
errorList.add(
|
||||
Text(
|
||||
errorHeaderMsg.replaceAll('_', ' '),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
|
||||
// Error messages
|
||||
if (exception.errors![key] is String) {
|
||||
errorList.add(Text(exception.errors![key]));
|
||||
} else {
|
||||
for (final value in exception.errors![key]) {
|
||||
errorList.add(Text(value));
|
||||
}
|
||||
}
|
||||
errorList.add(const SizedBox(height: 8));
|
||||
}
|
||||
final errorList = extractErrors(exception.errors);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
context: dialogContext,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(ctx).anErrorOccurred),
|
||||
content: Column(
|
||||
@@ -83,10 +70,6 @@ void showHttpExceptionErrorDialog(
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// This call serves no purpose The dialog above doesn't seem to show
|
||||
// unless this dummy call is present
|
||||
//showDialog(context: context, builder: (context) => Container());
|
||||
}
|
||||
|
||||
void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext? context}) {
|
||||
@@ -100,13 +83,12 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
if (dialogContext == null) {
|
||||
if (kDebugMode) {
|
||||
logger.warning('Error: Could not error show dialog because the context is null.');
|
||||
logger.warning('Original error: $error');
|
||||
logger.warning('Original stackTrace: $stackTrace');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the error title and message based on the error type.
|
||||
// If possible, determine the error title and message based on the error type.
|
||||
bool isNetworkError = false;
|
||||
String errorTitle = 'An error occurred';
|
||||
String errorMessage = error.toString();
|
||||
|
||||
@@ -118,7 +100,9 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
errorTitle = 'Application Error';
|
||||
errorMessage = error.exceptionAsString();
|
||||
} else if (error is MissingRequiredKeysException) {
|
||||
errorTitle = 'Missing Required Key ';
|
||||
errorTitle = 'Missing Required Key';
|
||||
} else if (error is SocketException) {
|
||||
isNetworkError = true;
|
||||
}
|
||||
|
||||
final String fullStackTrace = stackTrace?.toString() ?? 'No stack trace available.';
|
||||
@@ -133,11 +117,14 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error, color: Theme.of(context).colorScheme.error),
|
||||
Icon(
|
||||
isNetworkError ? Icons.signal_wifi_connected_no_internet_4_outlined : Icons.error,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
i18n.anErrorOccurred,
|
||||
isNetworkError ? i18n.errorCouldNotConnectToServer : i18n.anErrorOccurred,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
@@ -146,7 +133,11 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
Text(i18n.errorInfoDescription),
|
||||
Text(
|
||||
isNetworkError
|
||||
? i18n.errorCouldNotConnectToServerDetails
|
||||
: i18n.errorInfoDescription,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(i18n.errorInfoDescription2),
|
||||
const SizedBox(height: 10),
|
||||
@@ -198,36 +189,37 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Report issue'),
|
||||
onPressed: () async {
|
||||
const githubRepoUrl = 'https://github.com/wger-project/flutter';
|
||||
final description = Uri.encodeComponent(
|
||||
'## Description\n\n'
|
||||
'[Please describe what you were doing when the error occurred.]\n\n'
|
||||
'## Error details\n\n'
|
||||
'Error title: $errorTitle\n'
|
||||
'Error message: $errorMessage\n'
|
||||
'Stack trace:\n'
|
||||
'```\n$stackTrace\n```',
|
||||
);
|
||||
final githubIssueUrl = '$githubRepoUrl/issues/new?template=1_bug.yml'
|
||||
'&title=$errorTitle'
|
||||
'&description=$description';
|
||||
final Uri reportUri = Uri.parse(githubIssueUrl);
|
||||
|
||||
try {
|
||||
await launchUrl(reportUri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
if (kDebugMode) logger.warning('Error launching URL: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error opening issue tracker: $e')),
|
||||
if (!isNetworkError)
|
||||
TextButton(
|
||||
child: const Text('Report issue'),
|
||||
onPressed: () async {
|
||||
const githubRepoUrl = 'https://github.com/wger-project/flutter';
|
||||
final description = Uri.encodeComponent(
|
||||
'## Description\n\n'
|
||||
'[Please describe what you were doing when the error occurred.]\n\n'
|
||||
'## Error details\n\n'
|
||||
'Error title: $errorTitle\n'
|
||||
'Error message: $errorMessage\n'
|
||||
'Stack trace:\n'
|
||||
'```\n$stackTrace\n```',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
final githubIssueUrl = '$githubRepoUrl/issues/new?template=1_bug.yml'
|
||||
'&title=$errorTitle'
|
||||
'&description=$description';
|
||||
final Uri reportUri = Uri.parse(githubIssueUrl);
|
||||
|
||||
try {
|
||||
await launchUrl(reportUri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
if (kDebugMode) logger.warning('Error launching URL: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error opening issue tracker: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
FilledButton(
|
||||
child: const Text('OK'),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -279,3 +271,36 @@ void showDeleteDialog(BuildContext context, String confirmDeleteName, Log log) a
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
List<Widget> extractErrors(Map<String, dynamic>? errors) {
|
||||
final List<Widget> errorList = [];
|
||||
|
||||
if (errors == null) {
|
||||
return errorList;
|
||||
}
|
||||
|
||||
for (final key in errors.keys) {
|
||||
// Error headers
|
||||
// Ensure that the error heading first letter is capitalized.
|
||||
final String errorHeaderMsg = key[0].toUpperCase() + key.substring(1, key.length);
|
||||
|
||||
errorList.add(
|
||||
Text(
|
||||
errorHeaderMsg.replaceAll('_', ' '),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
|
||||
// Error messages
|
||||
if (errors[key] is String) {
|
||||
errorList.add(Text(errors[key]));
|
||||
} else {
|
||||
for (final value in errors[key]) {
|
||||
errorList.add(Text(value));
|
||||
}
|
||||
}
|
||||
errorList.add(const SizedBox(height: 8));
|
||||
}
|
||||
|
||||
return errorList;
|
||||
}
|
||||
@@ -303,8 +303,10 @@
|
||||
"goalFiber": "Fiber goal",
|
||||
"anErrorOccurred": "An Error Occurred!",
|
||||
"errorInfoDescription": "We're sorry, but something went wrong. You can help us fix this by reporting the issue on GitHub.",
|
||||
"errorInfoDescription2": "You can continue using the app, but some features may not work as expected.",
|
||||
"errorViewDetails": "View technical details",
|
||||
"errorInfoDescription2": "You can continue using the app, but some features may not work.",
|
||||
"errorViewDetails": "Technical details",
|
||||
"errorCouldNotConnectToServer": "Couldn't connect to server",
|
||||
"errorCouldNotConnectToServerDetails": "The application could not connect to the server. Please check your internet connection or the server URL and try again. If the problem persists, contact the server administrator.",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"weight": "Weight",
|
||||
"min": "Min",
|
||||
|
||||
@@ -22,6 +22,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/core/locator.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
@@ -60,7 +62,6 @@ import 'package:wger/theme/theme.dart';
|
||||
import 'package:wger/widgets/core/about.dart';
|
||||
import 'package:wger/widgets/core/settings.dart';
|
||||
|
||||
import 'helpers/ui.dart';
|
||||
import 'providers/auth.dart';
|
||||
|
||||
void _setupLogging() {
|
||||
@@ -73,14 +74,15 @@ void _setupLogging() {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
void main() async {
|
||||
// Needs to be called before runApp
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Logger
|
||||
_setupLogging();
|
||||
|
||||
final logger = Logger('main');
|
||||
//zx.setLogEnabled(kDebugMode);
|
||||
|
||||
// Needs to be called before runApp
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Locator to initialize exerciseDB
|
||||
await ServiceLocator().configure();
|
||||
|
||||
@@ -89,11 +91,12 @@ void main() async {
|
||||
|
||||
// Catch errors from Flutter itself (widget build, layout, paint, etc.)
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
final stack = details.stack ?? StackTrace.empty;
|
||||
if (kDebugMode) {
|
||||
FlutterError.dumpErrorToConsole(details);
|
||||
}
|
||||
showGeneralErrorDialog(details.exception, details.stack ?? StackTrace.empty);
|
||||
// Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.empty);
|
||||
|
||||
showGeneralErrorDialog(details.exception, stack);
|
||||
};
|
||||
|
||||
// Catch errors that happen outside of the Flutter framework (e.g., in async operations)
|
||||
@@ -102,7 +105,11 @@ void main() async {
|
||||
logger.warning('Caught error by PlatformDispatcher: $error');
|
||||
logger.warning('Stack trace: $stack');
|
||||
}
|
||||
showGeneralErrorDialog(error, stack);
|
||||
if (error is WgerHttpException) {
|
||||
showHttpExceptionErrorDialog(error);
|
||||
} else {
|
||||
showGeneralErrorDialog(error, stack);
|
||||
}
|
||||
|
||||
// Return true to indicate that the error has been handled.
|
||||
return true;
|
||||
|
||||
@@ -22,7 +22,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/screens/update_app_screen.dart';
|
||||
import 'package:wger/theme/theme.dart';
|
||||
@@ -195,24 +195,22 @@ class _AuthCardState extends State<AuthCard> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
} on WgerHttpException catch (error) {
|
||||
if (mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
if (mounted) {
|
||||
showGeneralErrorDialog(error, stackTrace, context: context);
|
||||
}
|
||||
rethrow;
|
||||
} catch (error) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/auth.dart';
|
||||
import 'package:wger/providers/body_weight.dart';
|
||||
@@ -101,7 +101,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
|
||||
} on WgerHttpException catch (error) {
|
||||
widget._logger.warning('Wger exception loading base data');
|
||||
if (mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/measurements/measurement_category.dart';
|
||||
import 'package:wger/models/measurements/measurement_entry.dart';
|
||||
@@ -123,7 +123,7 @@ class MeasurementCategoryForm extends StatelessWidget {
|
||||
);
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
@@ -296,7 +296,7 @@ class MeasurementEntryForm extends StatelessWidget {
|
||||
);
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
import 'package:wger/models/nutrition/log.dart';
|
||||
@@ -106,7 +106,7 @@ class MealForm extends StatelessWidget {
|
||||
listen: false,
|
||||
).editMeal(_meal);
|
||||
} on WgerHttpException catch (error) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -411,7 +411,7 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
);
|
||||
widget.onSave(context, _mealItem, date);
|
||||
} on WgerHttpException catch (error) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
@@ -690,7 +690,7 @@ class _PlanFormState extends State<PlanForm> {
|
||||
_descriptionController.clear();
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,8 +20,8 @@ import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart' as provider;
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/gym_mode.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
@@ -315,7 +315,7 @@ class _LogPageState extends State<LogPage> {
|
||||
_isSaving = false;
|
||||
} on WgerHttpException catch (error) {
|
||||
if (mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
_isSaving = false;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart' as provider;
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
@@ -237,7 +237,7 @@ class _SessionPageState extends State<SessionPage> {
|
||||
}
|
||||
} on WgerHttpException catch (error) {
|
||||
if (mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:wger/helpers/colors.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
|
||||
@@ -21,8 +21,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/helpers/ui.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/body_weight/weight_entry.dart';
|
||||
import 'package:wger/providers/body_weight.dart';
|
||||
@@ -179,7 +179,7 @@ class WeightForm extends StatelessWidget {
|
||||
: await provider.editEntry(_weightEntry);
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
showHttpExceptionErrorDialog(error, context);
|
||||
showHttpExceptionErrorDialog(error, context: context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
81
test/utils/errors_test.dart
Normal file
81
test/utils/errors_test.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
|
||||
void main() {
|
||||
group('extractErrors', () {
|
||||
testWidgets('Returns empty list when errors is null', (WidgetTester tester) async {
|
||||
final result = extractErrors(null);
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Returns empty list when errors is empty', (WidgetTester tester) async {
|
||||
final result = extractErrors({});
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Processes string values correctly', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final errors = {'error': 'Something went wrong'};
|
||||
|
||||
// Act
|
||||
final widgets = extractErrors(errors);
|
||||
|
||||
// Assert
|
||||
expect(widgets.length, 3, reason: 'Expected 3 widgets: header, message, and spacing');
|
||||
|
||||
final headerWidget = widgets[0] as Text;
|
||||
expect(headerWidget.data, 'Error');
|
||||
|
||||
final messageWidget = widgets[1] as Text;
|
||||
expect(messageWidget.data, 'Something went wrong');
|
||||
expect(widgets[2] is SizedBox, true);
|
||||
});
|
||||
|
||||
testWidgets('Processes list values correctly', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final errors = {
|
||||
'validation_error': ['Error 1', 'Error 2'],
|
||||
};
|
||||
|
||||
// Act
|
||||
final widgets = extractErrors(errors);
|
||||
|
||||
// Assert
|
||||
expect(widgets.length, 4);
|
||||
|
||||
final headerWidget = widgets[0] as Text;
|
||||
expect(headerWidget.data, 'Validation error');
|
||||
|
||||
final messageWidget1 = widgets[1] as Text;
|
||||
expect(messageWidget1.data, 'Error 1');
|
||||
|
||||
final messageWidget2 = widgets[2] as Text;
|
||||
expect(messageWidget2.data, 'Error 2');
|
||||
});
|
||||
|
||||
testWidgets('Processes multiple error types correctly', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final errors = {
|
||||
'username': ['Username is too boring', 'Username is too short'],
|
||||
'password': 'Password does not match',
|
||||
};
|
||||
|
||||
// Act
|
||||
final widgets = extractErrors(errors);
|
||||
|
||||
// Assert
|
||||
expect(widgets.length, 7);
|
||||
|
||||
final textWidgets = widgets.whereType<Text>().toList();
|
||||
expect(textWidgets.map((w) => w.data).contains('Username'), true);
|
||||
expect(textWidgets.map((w) => w.data).contains('Password'), true);
|
||||
expect(textWidgets.map((w) => w.data).contains('Username is too boring'), true);
|
||||
expect(textWidgets.map((w) => w.data).contains('Username is too short'), true);
|
||||
expect(textWidgets.map((w) => w.data).contains('Password does not match'), true);
|
||||
|
||||
final spacers = widgets.whereType<SizedBox>().toList();
|
||||
expect(spacers.length, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user