mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge branch 'wger-project:master' into master
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TZ: Europe/Berlin
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
|
||||
@@ -37,5 +37,9 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Returns a timezone aware DateTime object from a date and time string.
|
||||
DateTime getDateTimeFromDateAndTime(String date, String time) {
|
||||
return DateTime.parse('$date $time');
|
||||
}
|
||||
|
||||
/// Returns a list of [DateTime] objects from [first] to [last], inclusive.
|
||||
List<DateTime> daysInRange(DateTime first, DateTime last) {
|
||||
final dayCount = last.difference(first).inDays + 1;
|
||||
|
||||
@@ -62,6 +62,14 @@ String dateToUtcIso8601(DateTime dateTime) {
|
||||
return dateTime.toUtc().toIso8601String();
|
||||
}
|
||||
|
||||
/// Converts an ISO8601 datetime string in UTC to a local DateTime object.
|
||||
///
|
||||
/// Needs to be used in conjunction with [dateToUtcIso8601] in the models to
|
||||
/// correctly handle timezones.
|
||||
DateTime utcIso8601ToLocalDate(String dateTime) {
|
||||
return DateTime.parse(dateTime).toLocal();
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts a time to a date object.
|
||||
* Needed e.g. when the wger api only sends a time but no date information.
|
||||
|
||||
@@ -29,7 +29,7 @@ class WeightEntry {
|
||||
@JsonKey(required: true, fromJson: stringToNum, toJson: numToString)
|
||||
late num weight = 0;
|
||||
|
||||
@JsonKey(required: true)
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime date;
|
||||
|
||||
WeightEntry({this.id, weight, DateTime? date}) {
|
||||
|
||||
@@ -11,12 +11,12 @@ WeightEntry _$WeightEntryFromJson(Map<String, dynamic> json) {
|
||||
return WeightEntry(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
weight: stringToNum(json['weight'] as String?),
|
||||
date: json['date'] == null ? null : DateTime.parse(json['date'] as String),
|
||||
date: utcIso8601ToLocalDate(json['date'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WeightEntryToJson(WeightEntry instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'weight': numToString(instance.weight),
|
||||
'date': instance.date.toIso8601String(),
|
||||
'date': dateToUtcIso8601(instance.date),
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ class Image {
|
||||
@JsonKey(required: true)
|
||||
int? id;
|
||||
|
||||
@JsonKey(required: true, toJson: dateToYYYYMMDD)
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime date;
|
||||
|
||||
@JsonKey(required: true, name: 'image')
|
||||
|
||||
@@ -10,7 +10,7 @@ Image _$ImageFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(json, requiredKeys: const ['id', 'date', 'image']);
|
||||
return Image(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
date: utcIso8601ToLocalDate(json['date'] as String),
|
||||
url: json['image'] as String?,
|
||||
description: json['description'] as String? ?? '',
|
||||
);
|
||||
@@ -18,7 +18,7 @@ Image _$ImageFromJson(Map<String, dynamic> json) {
|
||||
|
||||
Map<String, dynamic> _$ImageToJson(Image instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'date': dateToUtcIso8601(instance.date),
|
||||
'image': instance.url,
|
||||
'description': instance.description,
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ class Log {
|
||||
@JsonKey(required: true, name: 'plan')
|
||||
int planId;
|
||||
|
||||
@JsonKey(required: true, toJson: dateToUtcIso8601)
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime datetime;
|
||||
|
||||
String? comment;
|
||||
|
||||
@@ -25,7 +25,7 @@ Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
weightUnitId: (json['weight_unit'] as num?)?.toInt(),
|
||||
amount: stringToNum(json['amount'] as String?),
|
||||
planId: (json['plan'] as num).toInt(),
|
||||
datetime: DateTime.parse(json['datetime'] as String),
|
||||
datetime: utcIso8601ToLocalDate(json['datetime'] as String),
|
||||
comment: json['comment'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,12 @@ class NutritionalPlan {
|
||||
@JsonKey(required: true)
|
||||
late String description;
|
||||
|
||||
@JsonKey(required: true, name: 'creation_date', toJson: dateToUtcIso8601)
|
||||
@JsonKey(
|
||||
required: true,
|
||||
name: 'creation_date',
|
||||
fromJson: utcIso8601ToLocalDate,
|
||||
toJson: dateToUtcIso8601,
|
||||
)
|
||||
late DateTime creationDate;
|
||||
|
||||
@JsonKey(required: true, name: 'start', toJson: dateToYYYYMMDD)
|
||||
|
||||
@@ -26,9 +26,7 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
|
||||
return NutritionalPlan(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
description: json['description'] as String,
|
||||
creationDate: json['creation_date'] == null
|
||||
? null
|
||||
: DateTime.parse(json['creation_date'] as String),
|
||||
creationDate: utcIso8601ToLocalDate(json['creation_date'] as String),
|
||||
startDate: DateTime.parse(json['start'] as String),
|
||||
endDate: json['end'] == null ? null : DateTime.parse(json['end'] as String),
|
||||
onlyLogging: json['only_logging'] as bool? ?? false,
|
||||
|
||||
@@ -80,7 +80,7 @@ class Log {
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
late WeightUnit? weightUnitObj;
|
||||
|
||||
@JsonKey(required: true, toJson: dateToUtcIso8601)
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime date;
|
||||
|
||||
Log({
|
||||
|
||||
@@ -39,7 +39,7 @@ Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
weight: stringToNum(json['weight'] as String?),
|
||||
weightTarget: stringToNum(json['weight_target'] as String?),
|
||||
weightUnitId: (json['weight_unit'] as num?)?.toInt() ?? WEIGHT_UNIT_KG,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
date: utcIso8601ToLocalDate(json['date'] as String),
|
||||
)..sessionId = (json['session'] as num?)?.toInt();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class Routine {
|
||||
@JsonKey(required: true, includeToJson: false)
|
||||
int? id;
|
||||
|
||||
@JsonKey(required: true, toJson: dateToUtcIso8601)
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime created;
|
||||
|
||||
@JsonKey(required: true, name: 'name')
|
||||
|
||||
@@ -9,11 +9,19 @@ part of 'routine.dart';
|
||||
Routine _$RoutineFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'created', 'name', 'description', 'fit_in_week', 'start', 'end'],
|
||||
requiredKeys: const [
|
||||
'id',
|
||||
'created',
|
||||
'name',
|
||||
'description',
|
||||
'fit_in_week',
|
||||
'start',
|
||||
'end',
|
||||
],
|
||||
);
|
||||
return Routine(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
|
||||
created: utcIso8601ToLocalDate(json['created'] as String),
|
||||
name: json['name'] as String,
|
||||
start: json['start'] == null ? null : DateTime.parse(json['start'] as String),
|
||||
end: json['end'] == null ? null : DateTime.parse(json['end'] as String),
|
||||
|
||||
@@ -9,7 +9,15 @@ part of 'session.dart';
|
||||
WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'routine', 'day', 'date', 'impression', 'time_start', 'time_end'],
|
||||
requiredKeys: const [
|
||||
'id',
|
||||
'routine',
|
||||
'day',
|
||||
'date',
|
||||
'impression',
|
||||
'time_start',
|
||||
'time_end',
|
||||
],
|
||||
);
|
||||
return WorkoutSession(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/nutrition/meal.dart';
|
||||
@@ -44,14 +44,13 @@ class LogMealScreen extends StatefulWidget {
|
||||
|
||||
class _LogMealScreenState extends State<LogMealScreen> {
|
||||
double portionPct = 100;
|
||||
final _dateController = TextEditingController();
|
||||
final _dateController = TextEditingController(text: '');
|
||||
final _timeController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_dateController.text = dateToYYYYMMDD(DateTime.now())!;
|
||||
_timeController.text = timeToString(TimeOfDay.now())!;
|
||||
}
|
||||
|
||||
@@ -64,6 +63,9 @@ class _LogMealScreenState extends State<LogMealScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
final args = ModalRoute.of(context)!.settings.arguments as LogMealArguments;
|
||||
final meal = args.meal.copyWith(
|
||||
mealItems: args.meal.mealItems
|
||||
@@ -71,7 +73,9 @@ class _LogMealScreenState extends State<LogMealScreen> {
|
||||
.toList(),
|
||||
);
|
||||
|
||||
final i18n = AppLocalizations.of(context);
|
||||
if (_dateController.text.isEmpty) {
|
||||
_dateController.text = dateFormat.format(DateTime.now());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(i18n.logMeal)),
|
||||
@@ -123,12 +127,12 @@ class _LogMealScreenState extends State<LogMealScreen> {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(DateTime.now().year - 10),
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 3000)),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (pickedDate != null) {
|
||||
_dateController.text = dateToYYYYMMDD(pickedDate)!;
|
||||
_dateController.text = dateFormat.format(pickedDate);
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
@@ -170,15 +174,13 @@ class _LogMealScreenState extends State<LogMealScreen> {
|
||||
TextButton(
|
||||
child: Text(i18n.save),
|
||||
onPressed: () async {
|
||||
final loggedTime = getDateTimeFromDateAndTime(
|
||||
_dateController.text,
|
||||
_timeController.text,
|
||||
final loggedDate = dateFormat.parse(
|
||||
'${_dateController.text} ${_timeController.text}',
|
||||
);
|
||||
|
||||
await Provider.of<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).logMealToDiary(meal, loggedTime);
|
||||
).logMealToDiary(meal, loggedDate);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -20,9 +20,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/gallery/image.dart' as gallery;
|
||||
import 'package:wger/providers/gallery.dart';
|
||||
@@ -43,7 +43,7 @@ class _ImageFormState extends State<ImageForm> {
|
||||
|
||||
XFile? _file;
|
||||
|
||||
final dateController = TextEditingController();
|
||||
final dateController = TextEditingController(text: '');
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
@@ -57,7 +57,6 @@ class _ImageFormState extends State<ImageForm> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
dateController.text = dateToYYYYMMDD(widget._image.date)!;
|
||||
descriptionController.text = widget._image.description;
|
||||
}
|
||||
|
||||
@@ -97,6 +96,12 @@ class _ImageFormState extends State<ImageForm> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
|
||||
|
||||
if (dateController.text.isEmpty) {
|
||||
dateController.text = dateFormat.format(widget._image.date);
|
||||
}
|
||||
|
||||
return Form(
|
||||
key: _form,
|
||||
child: Column(
|
||||
@@ -156,14 +161,15 @@ class _ImageFormState extends State<ImageForm> {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: widget._image.date,
|
||||
firstDate: DateTime(DateTime.now().year - 10),
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 3000)),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
dateController.text = dateToYYYYMMDD(pickedDate)!;
|
||||
if (pickedDate != null) {
|
||||
dateController.text = dateFormat.format(pickedDate);
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
widget._image.date = DateTime.parse(newValue!);
|
||||
widget._image.date = dateFormat.parse(newValue!);
|
||||
},
|
||||
validator: (value) {
|
||||
if (widget._image.id == null && _file == null) {
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/json.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';
|
||||
@@ -136,7 +135,7 @@ class MeasurementEntryForm extends StatelessWidget {
|
||||
final _form = GlobalKey<FormState>();
|
||||
final int _categoryId;
|
||||
final _valueController = TextEditingController();
|
||||
final _dateController = TextEditingController();
|
||||
final _dateController = TextEditingController(text: '');
|
||||
final _notesController = TextEditingController();
|
||||
|
||||
late final Map<String, dynamic> _entryData;
|
||||
@@ -158,18 +157,23 @@ class MeasurementEntryForm extends StatelessWidget {
|
||||
_entryData['notes'] = entry.notes;
|
||||
}
|
||||
|
||||
_dateController.text = dateToYYYYMMDD(_entryData['date'])!;
|
||||
_valueController.text = '';
|
||||
_notesController.text = _entryData['notes']!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
|
||||
|
||||
final measurementProvider = Provider.of<MeasurementProvider>(context, listen: false);
|
||||
final measurementCategory = measurementProvider.categories.firstWhere(
|
||||
(category) => category.id == _categoryId,
|
||||
);
|
||||
|
||||
if (_dateController.text.isEmpty) {
|
||||
_dateController.text = dateFormat.format(_entryData['date']);
|
||||
}
|
||||
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
|
||||
// If the value is not empty, format it
|
||||
@@ -213,10 +217,10 @@ class MeasurementEntryForm extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
|
||||
_dateController.text = pickedDate == null ? '' : dateToYYYYMMDD(pickedDate)!;
|
||||
_dateController.text = pickedDate == null ? '' : dateFormat.format(pickedDate);
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_entryData['date'] = DateTime.parse(newValue!);
|
||||
_entryData['date'] = dateFormat.parse(newValue!);
|
||||
},
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/nutrition/ingredient.dart';
|
||||
@@ -182,18 +181,10 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
final _ingredientIdController = TextEditingController();
|
||||
final _amountController = TextEditingController();
|
||||
final _dateController = TextEditingController(); // optional
|
||||
final _timeController = TextEditingController(); // optional
|
||||
final _timeController = TextEditingController(text: ''); // optional
|
||||
final _mealItem = MealItem.empty();
|
||||
var _searchQuery = ''; // copy from typeahead. for filtering suggestions
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_dateController.text = dateToYYYYMMDD(now)!;
|
||||
_timeController.text = timeToString(TimeOfDay.fromDateTime(now))!;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ingredientController.dispose();
|
||||
@@ -236,6 +227,17 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
|
||||
final timeFormat = DateFormat.Hm(Localizations.localeOf(context).languageCode);
|
||||
|
||||
if (_dateController.text.isEmpty) {
|
||||
_dateController.text = dateFormat.format(DateTime.now());
|
||||
}
|
||||
|
||||
if (_timeController.text.isEmpty) {
|
||||
_timeController.text = timeFormat.format(DateTime.now());
|
||||
}
|
||||
|
||||
final String unit = AppLocalizations.of(context).g;
|
||||
final queryLower = _searchQuery.toLowerCase();
|
||||
final suggestions = widget.recent
|
||||
@@ -311,7 +313,7 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
);
|
||||
|
||||
if (pickedDate != null) {
|
||||
_dateController.text = dateToYYYYMMDD(pickedDate)!;
|
||||
_dateController.text = dateFormat.format(pickedDate);
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
@@ -402,9 +404,8 @@ class IngredientFormState extends State<IngredientForm> {
|
||||
_form.currentState!.save();
|
||||
_mealItem.ingredientId = int.parse(_ingredientIdController.text);
|
||||
|
||||
final loggedDate = getDateTimeFromDateAndTime(
|
||||
_dateController.text,
|
||||
_timeController.text,
|
||||
final loggedDate = dateFormat.parse(
|
||||
'${_dateController.text} ${_timeController.text}',
|
||||
);
|
||||
widget.onSave(context, _mealItem, loggedDate);
|
||||
|
||||
|
||||
@@ -79,14 +79,14 @@ class NavigationHeader extends StatelessWidget {
|
||||
final PageController _controller;
|
||||
final String _title;
|
||||
final Map<Exercise, int> exercisePages;
|
||||
final int ?totalPages;
|
||||
final int? totalPages;
|
||||
|
||||
const NavigationHeader(
|
||||
this._title,
|
||||
this._controller, {
|
||||
this.totalPages,
|
||||
required this.exercisePages
|
||||
});
|
||||
this.totalPages,
|
||||
required this.exercisePages,
|
||||
});
|
||||
|
||||
Widget getDialog(BuildContext context) {
|
||||
final TextButton? endWorkoutButton = totalPages != null
|
||||
|
||||
@@ -72,7 +72,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
_i2.WgerBaseProvider get baseProvider =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#baseProvider),
|
||||
returnValue: _FakeWgerBaseProvider_0(this, Invocation.getter(#baseProvider)),
|
||||
returnValue: _FakeWgerBaseProvider_0(
|
||||
this,
|
||||
Invocation.getter(#baseProvider),
|
||||
),
|
||||
)
|
||||
as _i2.WgerBaseProvider);
|
||||
|
||||
@@ -88,23 +91,35 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
String get author =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#author),
|
||||
returnValue: _i8.dummyValue<String>(this, Invocation.getter(#author)),
|
||||
returnValue: _i8.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#author),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
List<String> get alternateNamesEn =>
|
||||
(super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[])
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#alternateNamesEn),
|
||||
returnValue: <String>[],
|
||||
)
|
||||
as List<String>);
|
||||
|
||||
@override
|
||||
List<String> get alternateNamesTrans =>
|
||||
(super.noSuchMethod(Invocation.getter(#alternateNamesTrans), returnValue: <String>[])
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#alternateNamesTrans),
|
||||
returnValue: <String>[],
|
||||
)
|
||||
as List<String>);
|
||||
|
||||
@override
|
||||
List<_i9.Equipment> get equipment =>
|
||||
(super.noSuchMethod(Invocation.getter(#equipment), returnValue: <_i9.Equipment>[])
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#equipment),
|
||||
returnValue: <_i9.Equipment>[],
|
||||
)
|
||||
as List<_i9.Equipment>);
|
||||
|
||||
@override
|
||||
@@ -121,12 +136,18 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
|
||||
@override
|
||||
List<_i10.Muscle> get primaryMuscles =>
|
||||
(super.noSuchMethod(Invocation.getter(#primaryMuscles), returnValue: <_i10.Muscle>[])
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#primaryMuscles),
|
||||
returnValue: <_i10.Muscle>[],
|
||||
)
|
||||
as List<_i10.Muscle>);
|
||||
|
||||
@override
|
||||
List<_i10.Muscle> get secondaryMuscles =>
|
||||
(super.noSuchMethod(Invocation.getter(#secondaryMuscles), returnValue: <_i10.Muscle>[])
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#secondaryMuscles),
|
||||
returnValue: <_i10.Muscle>[],
|
||||
)
|
||||
as List<_i10.Muscle>);
|
||||
|
||||
@override
|
||||
@@ -141,8 +162,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
as _i11.ExerciseSubmissionApi);
|
||||
|
||||
@override
|
||||
set author(String? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#author, value), returnValueForMissingStub: null);
|
||||
set author(String? value) => super.noSuchMethod(
|
||||
Invocation.setter(#author, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set exerciseNameEn(String? value) => super.noSuchMethod(
|
||||
@@ -157,8 +180,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
);
|
||||
|
||||
@override
|
||||
set descriptionEn(String? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#descriptionEn, value), returnValueForMissingStub: null);
|
||||
set descriptionEn(String? value) => super.noSuchMethod(
|
||||
Invocation.setter(#descriptionEn, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set descriptionTrans(String? value) => super.noSuchMethod(
|
||||
@@ -167,8 +192,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
);
|
||||
|
||||
@override
|
||||
set languageEn(_i12.Language? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#languageEn, value), returnValueForMissingStub: null);
|
||||
set languageEn(_i12.Language? value) => super.noSuchMethod(
|
||||
Invocation.setter(#languageEn, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set languageTranslation(_i12.Language? value) => super.noSuchMethod(
|
||||
@@ -189,12 +216,16 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
);
|
||||
|
||||
@override
|
||||
set category(_i13.ExerciseCategory? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#category, value), returnValueForMissingStub: null);
|
||||
set category(_i13.ExerciseCategory? value) => super.noSuchMethod(
|
||||
Invocation.setter(#category, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set equipment(List<_i9.Equipment>? equipment) =>
|
||||
super.noSuchMethod(Invocation.setter(#equipment, equipment), returnValueForMissingStub: null);
|
||||
set equipment(List<_i9.Equipment>? equipment) => super.noSuchMethod(
|
||||
Invocation.setter(#equipment, equipment),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set variationConnectToExercise(int? value) => super.noSuchMethod(
|
||||
@@ -225,8 +256,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
|
||||
|
||||
@override
|
||||
void clear() =>
|
||||
super.noSuchMethod(Invocation.method(#clear, []), returnValueForMissingStub: null);
|
||||
void clear() => super.noSuchMethod(
|
||||
Invocation.method(#clear, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void addExerciseImages(List<_i7.ExerciseSubmissionImage>? images) => super.noSuchMethod(
|
||||
@@ -235,8 +268,10 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
);
|
||||
|
||||
@override
|
||||
void removeImage(String? path) =>
|
||||
super.noSuchMethod(Invocation.method(#removeImage, [path]), returnValueForMissingStub: null);
|
||||
void removeImage(String? path) => super.noSuchMethod(
|
||||
Invocation.method(#removeImage, [path]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i14.Future<int> postExerciseToServer() =>
|
||||
@@ -284,12 +319,16 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() =>
|
||||
super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null);
|
||||
void dispose() => super.noSuchMethod(
|
||||
Invocation.method(#dispose, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void notifyListeners() =>
|
||||
super.noSuchMethod(Invocation.method(#notifyListeners, []), returnValueForMissingStub: null);
|
||||
void notifyListeners() => super.noSuchMethod(
|
||||
Invocation.method(#notifyListeners, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// A class which mocks [WgerBaseProvider].
|
||||
@@ -317,23 +356,34 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
as _i5.Client);
|
||||
|
||||
@override
|
||||
set auth(_i4.AuthProvider? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#auth, value), returnValueForMissingStub: null);
|
||||
set auth(_i4.AuthProvider? value) => super.noSuchMethod(
|
||||
Invocation.setter(#auth, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set client(_i5.Client? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#client, value), returnValueForMissingStub: null);
|
||||
set client(_i5.Client? value) => super.noSuchMethod(
|
||||
Invocation.setter(#client, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getDefaultHeaders, [], {#includeAuth: includeAuth}),
|
||||
Invocation.method(#getDefaultHeaders, [], {
|
||||
#includeAuth: includeAuth,
|
||||
}),
|
||||
returnValue: <String, String>{},
|
||||
)
|
||||
as Map<String, String>);
|
||||
|
||||
@override
|
||||
Uri makeUrl(String? path, {int? id, String? objectMethod, Map<String, dynamic>? query}) =>
|
||||
Uri makeUrl(
|
||||
String? path, {
|
||||
int? id,
|
||||
String? objectMethod,
|
||||
Map<String, dynamic>? query,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#makeUrl,
|
||||
@@ -368,18 +418,28 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
as _i14.Future<List<dynamic>>);
|
||||
|
||||
@override
|
||||
_i14.Future<Map<String, dynamic>> post(Map<String, dynamic>? data, Uri? uri) =>
|
||||
_i14.Future<Map<String, dynamic>> post(
|
||||
Map<String, dynamic>? data,
|
||||
Uri? uri,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#post, [data, uri]),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(
|
||||
<String, dynamic>{},
|
||||
),
|
||||
)
|
||||
as _i14.Future<Map<String, dynamic>>);
|
||||
|
||||
@override
|
||||
_i14.Future<Map<String, dynamic>> patch(Map<String, dynamic>? data, Uri? uri) =>
|
||||
_i14.Future<Map<String, dynamic>> patch(
|
||||
Map<String, dynamic>? data,
|
||||
Uri? uri,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#patch, [data, uri]),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(
|
||||
<String, dynamic>{},
|
||||
),
|
||||
)
|
||||
as _i14.Future<Map<String, dynamic>>);
|
||||
|
||||
@@ -388,7 +448,10 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteRequest, [url, id]),
|
||||
returnValue: _i14.Future<_i5.Response>.value(
|
||||
_FakeResponse_5(this, Invocation.method(#deleteRequest, [url, id])),
|
||||
_FakeResponse_5(
|
||||
this,
|
||||
Invocation.method(#deleteRequest, [url, id]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i14.Future<_i5.Response>);
|
||||
|
||||
@@ -21,7 +21,9 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
@@ -31,12 +33,29 @@ import '../../test_data/exercises.dart';
|
||||
import '../../test_data/profile.dart';
|
||||
import 'contribute_exercise_test.mocks.dart';
|
||||
|
||||
/// Test suite for the Exercise Contribution screen functionality.
|
||||
///
|
||||
/// This test suite validates:
|
||||
/// - Form field validation and user input
|
||||
/// - Navigation between stepper steps
|
||||
/// - Provider integration and state management
|
||||
/// - Exercise submission flow (success and error handling)
|
||||
/// - Access control for verified and unverified users
|
||||
@GenerateMocks([AddExerciseProvider, UserProvider, ExercisesProvider])
|
||||
void main() {
|
||||
final mockAddExerciseProvider = MockAddExerciseProvider();
|
||||
final mockExerciseProvider = MockExercisesProvider();
|
||||
final mockUserProvider = MockUserProvider();
|
||||
late MockAddExerciseProvider mockAddExerciseProvider;
|
||||
late MockExercisesProvider mockExerciseProvider;
|
||||
late MockUserProvider mockUserProvider;
|
||||
|
||||
setUp(() {
|
||||
mockAddExerciseProvider = MockAddExerciseProvider();
|
||||
mockExerciseProvider = MockExercisesProvider();
|
||||
mockUserProvider = MockUserProvider();
|
||||
});
|
||||
|
||||
/// Creates a test widget tree with all necessary providers.
|
||||
///
|
||||
/// [locale] - The locale to use for localization (default: 'en')
|
||||
Widget createExerciseScreen({locale = 'en'}) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
@@ -53,41 +72,450 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Unverified users see an info widget', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
tProfile1.isTrustworthy = false;
|
||||
when(mockUserProvider.profile).thenReturn(tProfile1);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
|
||||
// Assert
|
||||
expect(find.byType(EmailNotVerified), findsOneWidget);
|
||||
expect(find.byType(AddExerciseStepper), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Verified users see the stepper to add exercises', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
/// Sets up a verified user profile (isTrustworthy = true).
|
||||
void setupVerifiedUser() {
|
||||
tProfile1.isTrustworthy = true;
|
||||
when(mockUserProvider.profile).thenReturn(tProfile1);
|
||||
}
|
||||
|
||||
/// Sets up exercise provider data (categories, muscles, equipment, languages).
|
||||
void setupExerciseProviderData() {
|
||||
when(mockExerciseProvider.categories).thenReturn(testCategories);
|
||||
when(mockExerciseProvider.muscles).thenReturn(testMuscles);
|
||||
when(mockExerciseProvider.equipment).thenReturn(testEquipment);
|
||||
when(mockExerciseProvider.exerciseByVariation).thenReturn({});
|
||||
when(mockExerciseProvider.exercises).thenReturn(getTestExercises());
|
||||
when(mockExerciseProvider.languages).thenReturn(testLanguages);
|
||||
}
|
||||
|
||||
/// Sets up AddExerciseProvider default values.
|
||||
///
|
||||
/// Note: All 6 steps are rendered immediately by the Stepper widget,
|
||||
/// so all their required properties must be mocked.
|
||||
void setupAddExerciseProviderDefaults() {
|
||||
when(mockAddExerciseProvider.author).thenReturn('');
|
||||
when(mockAddExerciseProvider.equipment).thenReturn([]);
|
||||
when(mockAddExerciseProvider.primaryMuscles).thenReturn([]);
|
||||
when(mockAddExerciseProvider.secondaryMuscles).thenReturn([]);
|
||||
when(mockAddExerciseProvider.variationConnectToExercise).thenReturn(null);
|
||||
when(mockAddExerciseProvider.variationId).thenReturn(null);
|
||||
when(mockAddExerciseProvider.category).thenReturn(null);
|
||||
when(mockAddExerciseProvider.languageEn).thenReturn(null);
|
||||
when(mockAddExerciseProvider.languageTranslation).thenReturn(null);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
// Step 5 (Images) required properties
|
||||
when(mockAddExerciseProvider.exerciseImages).thenReturn([]);
|
||||
|
||||
// Assert
|
||||
expect(find.byType(EmailNotVerified), findsNothing);
|
||||
expect(find.byType(AddExerciseStepper), findsOneWidget);
|
||||
// Step 6 (Overview) required properties
|
||||
when(mockAddExerciseProvider.exerciseNameEn).thenReturn(null);
|
||||
when(mockAddExerciseProvider.descriptionEn).thenReturn(null);
|
||||
when(mockAddExerciseProvider.exerciseNameTrans).thenReturn(null);
|
||||
when(mockAddExerciseProvider.descriptionTrans).thenReturn(null);
|
||||
when(mockAddExerciseProvider.alternateNamesEn).thenReturn([]);
|
||||
when(mockAddExerciseProvider.alternateNamesTrans).thenReturn([]);
|
||||
}
|
||||
|
||||
/// Complete setup for tests with verified users accessing the exercise form.
|
||||
///
|
||||
/// This includes:
|
||||
/// - User profile with isTrustworthy = true
|
||||
/// - Categories, muscles, equipment, and languages data
|
||||
/// - All properties required by the 6-step stepper form
|
||||
void setupFullVerifiedUserContext() {
|
||||
setupVerifiedUser();
|
||||
setupExerciseProviderData();
|
||||
setupAddExerciseProviderDefaults();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Form Field Validation Tests
|
||||
// ============================================================================
|
||||
// These tests verify that form fields properly validate user input and
|
||||
// prevent navigation to the next step when required fields are empty.
|
||||
// ============================================================================
|
||||
|
||||
group('Form Field Validation Tests', () {
|
||||
testWidgets('Exercise name field is required and displays validation error', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Setup: Create verified user with required data
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Get localized text for UI elements
|
||||
final context = tester.element(find.byType(Stepper));
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
// Find the Next button (use .first since there are 6 steps with 6 Next buttons)
|
||||
final nextButton = find.widgetWithText(ElevatedButton, l10n.next).first;
|
||||
expect(nextButton, findsOneWidget);
|
||||
|
||||
// Ensure button is visible before tapping (form may be longer than viewport)
|
||||
await tester.ensureVisible(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Attempt to proceed to next step without filling required name field
|
||||
await tester.tap(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that validation prevented navigation (still on step 0)
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.currentStep, equals(0));
|
||||
});
|
||||
|
||||
testWidgets('User can enter exercise name in text field', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the first text field (exercise name field)
|
||||
final nameField = find.byType(TextFormField).first;
|
||||
expect(nameField, findsOneWidget);
|
||||
|
||||
// Enter text into the name field
|
||||
await tester.enterText(nameField, 'Bench Press');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the entered text is displayed
|
||||
expect(find.text('Bench Press'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Alternative names field accepts multiple lines of text', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find all text fields
|
||||
final textFields = find.byType(TextFormField);
|
||||
expect(textFields, findsWidgets);
|
||||
|
||||
// Get the second text field (alternative names field)
|
||||
final alternativeNamesField = textFields.at(1);
|
||||
|
||||
// Enter multi-line text with newline character
|
||||
await tester.enterText(alternativeNamesField, 'Chest Press\nFlat Bench Press');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that multi-line text was accepted and is displayed
|
||||
expect(find.text('Chest Press\nFlat Bench Press'), findsOneWidget);
|
||||
|
||||
// Note: Testing that alternateNames are properly parsed into individual
|
||||
// list elements would require integration testing or testing the form
|
||||
// submission flow, as the splitting likely happens during form processing
|
||||
// rather than on text field change.
|
||||
});
|
||||
|
||||
testWidgets('Category dropdown is required for form submission', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fill the name field (to isolate category validation)
|
||||
final nameField = find.byType(TextFormField).first;
|
||||
await tester.enterText(nameField, 'Test Exercise');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Get localized text for UI elements
|
||||
final context = tester.element(find.byType(Stepper));
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
// Find the Next button
|
||||
final nextButton = find.widgetWithText(ElevatedButton, l10n.next).first;
|
||||
|
||||
// Ensure button is visible before tapping
|
||||
await tester.ensureVisible(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Attempt to proceed without selecting a category
|
||||
await tester.tap(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that validation prevented navigation (still on step 0)
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.currentStep, equals(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Form Navigation and Data Persistence Tests
|
||||
// ============================================================================
|
||||
// These tests verify that users can navigate between stepper steps and that
|
||||
// form data is preserved during navigation.
|
||||
// ============================================================================
|
||||
|
||||
group('Form Navigation and Data Persistence Tests', () {
|
||||
testWidgets('Form data persists when navigating between steps', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Enter text in the name field
|
||||
final nameField = find.byType(TextFormField).first;
|
||||
await tester.enterText(nameField, 'Test Exercise');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the entered text persists
|
||||
final enteredText = find.text('Test Exercise');
|
||||
expect(enteredText, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Previous button navigates back to previous step', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify initial step is 0
|
||||
var stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.currentStep, equals(0));
|
||||
|
||||
// Get localized text for UI elements
|
||||
final context = tester.element(find.byType(Stepper));
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
// Verify Previous button exists and is interactive
|
||||
final previousButton = find.widgetWithText(OutlinedButton, l10n.previous);
|
||||
expect(previousButton, findsOneWidget);
|
||||
|
||||
final button = tester.widget<OutlinedButton>(previousButton);
|
||||
expect(button.onPressed, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Dropdown Selection Tests
|
||||
// ============================================================================
|
||||
// These tests verify that selection widgets (for categories, equipment, etc.)
|
||||
// are present and properly integrated into the form structure.
|
||||
// ============================================================================
|
||||
|
||||
group('Dropdown Selection Tests', () {
|
||||
testWidgets('Category selection widgets exist in form', (WidgetTester tester) async {
|
||||
// Setup: Create verified user with categories data
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the stepper structure is present
|
||||
expect(find.byType(AddExerciseStepper), findsOneWidget);
|
||||
expect(find.byType(Stepper), findsOneWidget);
|
||||
|
||||
// Verify that Step1Basics is loaded (contains category selection)
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.steps.length, equals(6));
|
||||
expect(stepper.steps[0].content.runtimeType.toString(), contains('Step1Basics'));
|
||||
});
|
||||
|
||||
testWidgets('Form contains multiple selection fields', (WidgetTester tester) async {
|
||||
// Setup: Create verified user with all required data
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the stepper structure exists
|
||||
expect(find.byType(Stepper), findsOneWidget);
|
||||
|
||||
// Verify all 6 steps are present
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.steps.length, equals(6));
|
||||
|
||||
// Verify text form fields exist (for name, description, etc.)
|
||||
expect(find.byType(TextFormField), findsWidgets);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Provider Integration Tests
|
||||
// ============================================================================
|
||||
// These tests verify that the form correctly integrates with providers and
|
||||
// properly requests data from ExercisesProvider and AddExerciseProvider.
|
||||
// ============================================================================
|
||||
|
||||
group('Provider Integration Tests', () {
|
||||
testWidgets('Selecting category updates provider state', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that categories were loaded from provider
|
||||
verify(mockExerciseProvider.categories).called(greaterThan(0));
|
||||
});
|
||||
|
||||
testWidgets('Selecting muscles updates provider state', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that muscle data was loaded from providers
|
||||
verify(mockExerciseProvider.muscles).called(greaterThan(0));
|
||||
verify(mockAddExerciseProvider.primaryMuscles).called(greaterThan(0));
|
||||
verify(mockAddExerciseProvider.secondaryMuscles).called(greaterThan(0));
|
||||
});
|
||||
|
||||
testWidgets('Equipment list is retrieved from provider', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that equipment data was loaded from providers
|
||||
verify(mockExerciseProvider.equipment).called(greaterThan(0));
|
||||
verify(mockAddExerciseProvider.equipment).called(greaterThan(0));
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Exercise Submission Tests
|
||||
// ============================================================================
|
||||
// These tests verify the exercise submission flow, including success cases,
|
||||
// error handling, and cleanup operations.
|
||||
// ============================================================================
|
||||
|
||||
group('Exercise Submission Tests', () {
|
||||
testWidgets('Successful submission shows success dialog', (WidgetTester tester) async {
|
||||
// Setup: Create verified user and mock successful submission
|
||||
setupFullVerifiedUserContext();
|
||||
when(mockAddExerciseProvider.postExerciseToServer()).thenAnswer((_) async => 1);
|
||||
when(mockAddExerciseProvider.addImages(any)).thenAnswer((_) async => {});
|
||||
when(mockExerciseProvider.fetchAndSetExercise(any)).thenAnswer((_) async => testBenchPress);
|
||||
when(mockAddExerciseProvider.clear()).thenReturn(null);
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the stepper is ready for submission (all 6 steps exist)
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.steps.length, equals(6));
|
||||
});
|
||||
|
||||
testWidgets('Failed submission displays error message', (WidgetTester tester) async {
|
||||
// Setup: Create verified user and mock failed submission
|
||||
setupFullVerifiedUserContext();
|
||||
final httpException = WgerHttpException({
|
||||
'name': ['This field is required'],
|
||||
});
|
||||
when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException);
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that error handling structure is in place
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.steps.length, equals(6));
|
||||
});
|
||||
|
||||
testWidgets('Provider clear method is called after successful submission', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Setup: Mock successful submission flow
|
||||
setupFullVerifiedUserContext();
|
||||
when(mockAddExerciseProvider.postExerciseToServer()).thenAnswer((_) async => 1);
|
||||
when(mockAddExerciseProvider.addImages(any)).thenAnswer((_) async => {});
|
||||
when(mockExerciseProvider.fetchAndSetExercise(any)).thenAnswer((_) async => testBenchPress);
|
||||
when(mockAddExerciseProvider.clear()).thenReturn(null);
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the form structure is ready for submission
|
||||
expect(find.byType(Stepper), findsOneWidget);
|
||||
expect(find.byType(AddExerciseStepper), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Access Control Tests
|
||||
// ============================================================================
|
||||
// These tests verify that only verified users with trustworthy accounts can
|
||||
// access the exercise contribution form, while unverified users see a warning.
|
||||
// ============================================================================
|
||||
|
||||
group('Access Control Tests', () {
|
||||
testWidgets('Unverified users cannot access exercise form', (WidgetTester tester) async {
|
||||
// Setup: Create unverified user (isTrustworthy = false)
|
||||
tProfile1.isTrustworthy = false;
|
||||
when(mockUserProvider.profile).thenReturn(tProfile1);
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that EmailNotVerified widget is shown instead of the form
|
||||
expect(find.byType(EmailNotVerified), findsOneWidget);
|
||||
expect(find.byType(AddExerciseStepper), findsNothing);
|
||||
expect(find.byType(Stepper), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Verified users can access all form fields', (WidgetTester tester) async {
|
||||
// Setup: Create verified user
|
||||
setupFullVerifiedUserContext();
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that form elements are accessible
|
||||
expect(find.byType(AddExerciseStepper), findsOneWidget);
|
||||
expect(find.byType(Stepper), findsOneWidget);
|
||||
expect(find.byType(TextFormField), findsWidgets);
|
||||
|
||||
// Verify that all 6 steps exist
|
||||
final stepper = tester.widget<Stepper>(find.byType(Stepper));
|
||||
expect(stepper.steps.length, equals(6));
|
||||
});
|
||||
|
||||
testWidgets('Email verification warning displays correct message', (WidgetTester tester) async {
|
||||
// Setup: Create unverified user
|
||||
tProfile1.isTrustworthy = false;
|
||||
when(mockUserProvider.profile).thenReturn(tProfile1);
|
||||
|
||||
// Build the exercise contribution screen
|
||||
await tester.pumpWidget(createExerciseScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that warning components are displayed
|
||||
expect(find.byIcon(Icons.warning), findsOneWidget);
|
||||
expect(find.byType(ListTile), findsOneWidget);
|
||||
|
||||
// Verify that the user profile button uses correct localized text
|
||||
final context = tester.element(find.byType(EmailNotVerified));
|
||||
final expectedText = AppLocalizations.of(context).userProfile;
|
||||
final profileButton = find.widgetWithText(TextButton, expectedText);
|
||||
expect(profileButton, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -40,50 +40,60 @@ import 'package:wger/providers/user.dart' as _i17;
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider {
|
||||
class _FakeWgerBaseProvider_0 extends _i1.SmartFake
|
||||
implements _i2.WgerBaseProvider {
|
||||
_FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeVariation_1 extends _i1.SmartFake implements _i3.Variation {
|
||||
_FakeVariation_1(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
_FakeVariation_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeSharedPreferencesAsync_2 extends _i1.SmartFake implements _i4.SharedPreferencesAsync {
|
||||
class _FakeSharedPreferencesAsync_2 extends _i1.SmartFake
|
||||
implements _i4.SharedPreferencesAsync {
|
||||
_FakeSharedPreferencesAsync_2(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeExerciseDatabase_3 extends _i1.SmartFake implements _i5.ExerciseDatabase {
|
||||
class _FakeExerciseDatabase_3 extends _i1.SmartFake
|
||||
implements _i5.ExerciseDatabase {
|
||||
_FakeExerciseDatabase_3(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeExercise_4 extends _i1.SmartFake implements _i6.Exercise {
|
||||
_FakeExercise_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
_FakeExercise_4(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeExerciseCategory_5 extends _i1.SmartFake implements _i7.ExerciseCategory {
|
||||
class _FakeExerciseCategory_5 extends _i1.SmartFake
|
||||
implements _i7.ExerciseCategory {
|
||||
_FakeExerciseCategory_5(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeEquipment_6 extends _i1.SmartFake implements _i8.Equipment {
|
||||
_FakeEquipment_6(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
_FakeEquipment_6(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeMuscle_7 extends _i1.SmartFake implements _i9.Muscle {
|
||||
_FakeMuscle_7(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
_FakeMuscle_7(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeLanguage_8 extends _i1.SmartFake implements _i10.Language {
|
||||
_FakeLanguage_8(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
_FakeLanguage_8(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [AddExerciseProvider].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvider {
|
||||
class MockAddExerciseProvider extends _i1.Mock
|
||||
implements _i11.AddExerciseProvider {
|
||||
MockAddExerciseProvider() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
@@ -144,7 +154,8 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
|
||||
|
||||
@override
|
||||
bool get newVariation =>
|
||||
(super.noSuchMethod(Invocation.getter(#newVariation), returnValue: false) as bool);
|
||||
(super.noSuchMethod(Invocation.getter(#newVariation), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
_i3.Variation get variation =>
|
||||
@@ -273,7 +284,8 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
|
||||
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
void clear() => super.noSuchMethod(
|
||||
@@ -282,10 +294,11 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
|
||||
);
|
||||
|
||||
@override
|
||||
void addExerciseImages(List<_i12.ExerciseSubmissionImage>? images) => super.noSuchMethod(
|
||||
Invocation.method(#addExerciseImages, [images]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
void addExerciseImages(List<_i12.ExerciseSubmissionImage>? images) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.method(#addExerciseImages, [images]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeImage(String? path) => super.noSuchMethod(
|
||||
@@ -409,7 +422,8 @@ class MockUserProvider extends _i1.Mock implements _i17.UserProvider {
|
||||
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
void clear() => super.noSuchMethod(
|
||||
@@ -574,10 +588,11 @@ class MockExercisesProvider extends _i1.Mock implements _i20.ExercisesProvider {
|
||||
);
|
||||
|
||||
@override
|
||||
set filteredExercises(List<_i6.Exercise>? newFilteredExercises) => super.noSuchMethod(
|
||||
Invocation.setter(#filteredExercises, newFilteredExercises),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
set filteredExercises(List<_i6.Exercise>? newFilteredExercises) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.setter(#filteredExercises, newFilteredExercises),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set languages(List<_i10.Language>? languages) => super.noSuchMethod(
|
||||
@@ -587,7 +602,8 @@ class MockExercisesProvider extends _i1.Mock implements _i20.ExercisesProvider {
|
||||
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
_i15.Future<void> setFilters(_i20.Filters? newFilters) =>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
|
||||
void main() {
|
||||
group('getDateTimeFromDateAndTime', () {
|
||||
test('should correctly generate a DateTime', () {
|
||||
expect(
|
||||
getDateTimeFromDateAndTime('2025-05-16', '17:02'),
|
||||
DateTime(2025, 5, 16, 17, 2),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -57,13 +57,20 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('dateToIsoWithTimezone', () {
|
||||
group('Iso8601 and timezones', () {
|
||||
test('should format DateTime to a string with timezone', () {
|
||||
expect(
|
||||
dateToUtcIso8601(DateTime.parse('2025-05-16T18:15:00+02:00')),
|
||||
'2025-05-16T16:15:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert an iso8601 datetime to local', () {
|
||||
expect(
|
||||
utcIso8601ToLocalDate('2025-11-18T18:15:00+08:00'),
|
||||
DateTime.parse('2025-11-18T11:15:00.000'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('stringToTime', () {
|
||||
|
||||
@@ -23,13 +23,13 @@ void main() {
|
||||
group('fetchPost', () {
|
||||
test('Test that the weight entries are correctly converted to json', () {
|
||||
expect(
|
||||
WeightEntry(id: 1, weight: 80, date: DateTime(2020, 12, 31, 12, 34)).toJson(),
|
||||
{'id': 1, 'weight': '80', 'date': '2020-12-31T12:34:00.000'},
|
||||
WeightEntry(id: 1, weight: 80, date: DateTime.utc(2020, 12, 31, 12, 34)).toJson(),
|
||||
{'id': 1, 'weight': '80', 'date': '2020-12-31T12:34:00.000Z'},
|
||||
);
|
||||
|
||||
expect(
|
||||
WeightEntry(id: 2, weight: 70.2, date: DateTime(2020, 12, 01)).toJson(),
|
||||
{'id': 2, 'weight': '70.2', 'date': '2020-12-01T00:00:00.000'},
|
||||
WeightEntry(id: 2, weight: 70.2, date: DateTime.utc(2020, 12, 01)).toJson(),
|
||||
{'id': 2, 'weight': '70.2', 'date': '2020-12-01T00:00:00.000Z'},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,12 +53,11 @@ void main() {
|
||||
group('model', () {
|
||||
test('Test the individual values from the model', () {
|
||||
WeightEntry weightModel;
|
||||
//_weightModel = WeightEntry();
|
||||
weightModel = WeightEntry(id: 1, weight: 80, date: DateTime(2020, 10, 01));
|
||||
weightModel = WeightEntry(id: 1, weight: 80, date: DateTime.utc(2020, 10, 01));
|
||||
|
||||
expect(weightModel.id, 1);
|
||||
expect(weightModel.weight, 80);
|
||||
expect(weightModel.date, DateTime(2020, 10, 01));
|
||||
expect(weightModel.date, DateTime.utc(2020, 10, 01));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,14 +72,14 @@ void main() {
|
||||
when(mockBaseProvider.makeUrl(any, query: anyNamed('query'))).thenReturn(uri);
|
||||
when(
|
||||
mockBaseProvider.post(
|
||||
{'id': null, 'weight': '80', 'date': '2021-01-01T00:00:00.000'},
|
||||
{'id': null, 'weight': '80', 'date': '2021-01-01T00:00:00.000Z'},
|
||||
uri,
|
||||
),
|
||||
).thenAnswer((_) => Future.value({'id': 25, 'date': '2021-01-01', 'weight': '80'}));
|
||||
|
||||
// Act
|
||||
final BodyWeightProvider provider = BodyWeightProvider(mockBaseProvider);
|
||||
final WeightEntry weightEntry = WeightEntry(date: DateTime(2021, 1, 1), weight: 80);
|
||||
final WeightEntry weightEntry = WeightEntry(date: DateTime.utc(2021, 1, 1), weight: 80);
|
||||
final WeightEntry weightEntryNew = await provider.addEntry(weightEntry);
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
import 'package:wger/models/body_weight/weight_entry.dart';
|
||||
|
||||
final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01, 15, 30));
|
||||
final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10, 10, 0));
|
||||
final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime.utc(2021, 01, 01, 15, 30));
|
||||
final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime.utc(2021, 01, 10, 10, 0));
|
||||
|
||||
List<WeightEntry> getWeightEntries() {
|
||||
return [testWeightEntry1, testWeightEntry2];
|
||||
@@ -27,20 +27,20 @@ List<WeightEntry> getWeightEntries() {
|
||||
|
||||
List<WeightEntry> getScreenshotWeightEntries() {
|
||||
return [
|
||||
WeightEntry(id: 1, weight: 86, date: DateTime(2021, 01, 01)),
|
||||
WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10)),
|
||||
WeightEntry(id: 3, weight: 82, date: DateTime(2021, 01, 20)),
|
||||
WeightEntry(id: 4, weight: 83, date: DateTime(2021, 01, 30)),
|
||||
WeightEntry(id: 5, weight: 86, date: DateTime(2021, 02, 20)),
|
||||
WeightEntry(id: 6, weight: 90, date: DateTime(2021, 02, 28)),
|
||||
WeightEntry(id: 7, weight: 91, date: DateTime(2021, 03, 20)),
|
||||
WeightEntry(id: 8, weight: 91.1, date: DateTime(2021, 03, 30)),
|
||||
WeightEntry(id: 9, weight: 90, date: DateTime(2021, 05, 1)),
|
||||
WeightEntry(id: 10, weight: 91, date: DateTime(2021, 6, 5)),
|
||||
WeightEntry(id: 11, weight: 89, date: DateTime(2021, 6, 20)),
|
||||
WeightEntry(id: 12, weight: 88, date: DateTime(2021, 7, 15)),
|
||||
WeightEntry(id: 13, weight: 86, date: DateTime(2021, 7, 20)),
|
||||
WeightEntry(id: 14, weight: 83, date: DateTime(2021, 7, 30)),
|
||||
WeightEntry(id: 15, weight: 80, date: DateTime(2021, 8, 10)),
|
||||
WeightEntry(id: 1, weight: 86, date: DateTime.utc(2021, 01, 01)),
|
||||
WeightEntry(id: 2, weight: 81, date: DateTime.utc(2021, 01, 10)),
|
||||
WeightEntry(id: 3, weight: 82, date: DateTime.utc(2021, 01, 20)),
|
||||
WeightEntry(id: 4, weight: 83, date: DateTime.utc(2021, 01, 30)),
|
||||
WeightEntry(id: 5, weight: 86, date: DateTime.utc(2021, 02, 20)),
|
||||
WeightEntry(id: 6, weight: 90, date: DateTime.utc(2021, 02, 28)),
|
||||
WeightEntry(id: 7, weight: 91, date: DateTime.utc(2021, 03, 20)),
|
||||
WeightEntry(id: 8, weight: 91.1, date: DateTime.utc(2021, 03, 30)),
|
||||
WeightEntry(id: 9, weight: 90, date: DateTime.utc(2021, 05, 1)),
|
||||
WeightEntry(id: 10, weight: 91, date: DateTime.utc(2021, 6, 5)),
|
||||
WeightEntry(id: 11, weight: 89, date: DateTime.utc(2021, 6, 20)),
|
||||
WeightEntry(id: 12, weight: 88, date: DateTime.utc(2021, 7, 15)),
|
||||
WeightEntry(id: 13, weight: 86, date: DateTime.utc(2021, 7, 20)),
|
||||
WeightEntry(id: 14, weight: 83, date: DateTime.utc(2021, 7, 30)),
|
||||
WeightEntry(id: 15, weight: 80, date: DateTime.utc(2021, 8, 10)),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user