mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
These need to be handled separately when the server encounters an error and returns HTML instead of JSON.
521 lines
22 KiB
Dart
521 lines
22 KiB
Dart
/*
|
|
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
|
* Copyright (C) 2020, 2021 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/material.dart';
|
|
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/core/exceptions/http_exception.dart';
|
|
import 'package:wger/l10n/generated/app_localizations.dart';
|
|
import 'package:wger/providers/add_exercise.dart';
|
|
import 'package:wger/providers/exercises.dart';
|
|
import 'package:wger/providers/user.dart';
|
|
import 'package:wger/screens/add_exercise_screen.dart';
|
|
|
|
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() {
|
|
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: [
|
|
ChangeNotifierProvider<ExercisesProvider>(create: (context) => mockExerciseProvider),
|
|
ChangeNotifierProvider<AddExerciseProvider>(create: (context) => mockAddExerciseProvider),
|
|
ChangeNotifierProvider<UserProvider>(create: (context) => mockUserProvider),
|
|
],
|
|
child: MaterialApp(
|
|
locale: Locale(locale),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: const AddExerciseScreen(),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 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);
|
|
|
|
// Step 5 (Images) required properties
|
|
when(mockAddExerciseProvider.exerciseImages).thenReturn([]);
|
|
|
|
// 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.fromMap({
|
|
'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);
|
|
});
|
|
});
|
|
}
|