From 0653cdb6d7fc3eeea99b98679b8f98b3b60d9b82 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Mon, 6 Oct 2025 08:58:36 +0330 Subject: [PATCH] changes --- lib/main.dart | 89 +++++- lib/view_models/auth_view_model.dart | 8 +- lib/view_models/profile_view_model.dart | 78 +++++ lib/views/profile/pages/m_profile_page.dart | 28 ++ test/fixtures/test_fixtures.dart | 283 ++++++++++++++++++ test/mocks/mock_auth_repository.dart | 97 ++++++ test/mocks/mock_your_tenders_repository.dart | 37 +++ .../auth_view_model_forgot_password_test.dart | 107 +++++++ .../auth_view_model_initialization_test.dart | 62 ++++ .../auth_view_model_login_test.dart | 86 ++++++ .../auth_view_model_logout_test.dart | 84 ++++++ .../view_models/auth_view_model_otp_test.dart | 122 ++++++++ .../auth_view_model_reset_password_test.dart | 140 +++++++++ .../auth_view_model_utility_test.dart | 128 ++++++++ .../auth_view_model_validation_test.dart | 94 ++++++ ...your_tenders_view_model_feedback_test.dart | 119 ++++++++ .../your_tenders_view_model_filter_test.dart | 137 +++++++++ ...r_tenders_view_model_get_tenders_test.dart | 146 +++++++++ ...enders_view_model_initialization_test.dart | 45 +++ ...ur_tenders_view_model_pagination_test.dart | 116 +++++++ .../your_tenders_view_model_utility_test.dart | 199 ++++++++++++ 21 files changed, 2190 insertions(+), 15 deletions(-) create mode 100644 test/fixtures/test_fixtures.dart create mode 100644 test/mocks/mock_auth_repository.dart create mode 100644 test/mocks/mock_your_tenders_repository.dart create mode 100644 test/view_models/auth_view_model_forgot_password_test.dart create mode 100644 test/view_models/auth_view_model_initialization_test.dart create mode 100644 test/view_models/auth_view_model_login_test.dart create mode 100644 test/view_models/auth_view_model_logout_test.dart create mode 100644 test/view_models/auth_view_model_otp_test.dart create mode 100644 test/view_models/auth_view_model_reset_password_test.dart create mode 100644 test/view_models/auth_view_model_utility_test.dart create mode 100644 test/view_models/auth_view_model_validation_test.dart create mode 100644 test/view_models/your_tenders_view_model_feedback_test.dart create mode 100644 test/view_models/your_tenders_view_model_filter_test.dart create mode 100644 test/view_models/your_tenders_view_model_get_tenders_test.dart create mode 100644 test/view_models/your_tenders_view_model_initialization_test.dart create mode 100644 test/view_models/your_tenders_view_model_pagination_test.dart create mode 100644 test/view_models/your_tenders_view_model_utility_test.dart diff --git a/lib/main.dart b/lib/main.dart index ea5eace..c33f307 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,25 +63,88 @@ class _MyAppState extends State { _handleMessage(initialMessage); } - if (kIsWeb) { - FirebaseMessaging.onMessage.listen((event) { - final overlayCtx = rootNavigatorKey.currentContext; - if (overlayCtx != null) { - if (overlayCtx.mounted) { - AppToast.notification( - overlayCtx, - event.notification?.title ?? 'New Notification', - event.notification?.body ?? 'New Notification', - ); - } + // if (kIsWeb) { + FirebaseMessaging.onMessage.listen((event) { + final overlayCtx = rootNavigatorKey.currentContext; + AppLogger().error('Notification received: ${event.notification?.web}'); + AppLogger().error('web link: ${event.notification?.title}'); + AppLogger().error('web link: ${event.notification?.web?.link}'); + print('Notification received1: ${event.notification?.web.toString()}'); + print( + 'Notification received1: ${event.notification?.web?.analyticsLabel}', + ); + + print('web link: ${event.notification?.web?.link}'); + print('Notification received: ${event.notification?.title}'); + AppLogger().error('notif data: ${event.data}'); + AppLogger().error('notif from: ${event.from}'); + AppLogger().error('notif type: ${event.messageType}'); + AppLogger().error('notif keys: ${event.data.keys}'); + AppLogger().info('notif title: ${event.notification?.title}'); + AppLogger().info('notif body: ${event.notification?.body}'); + AppLogger().info('notif data: ${event.data}'); + AppLogger().info('notif from: ${event.from}'); + AppLogger().info('notif type: ${event.messageType}'); + AppLogger().info('notif keys: ${event.data.keys}'); + if (overlayCtx != null) { + if (overlayCtx.mounted) { + AppLogger().error( + 'Notification received: ${event.notification?.web}', + ); + AppLogger().error( + ' link android: ${event.notification?.android?.link}', + ); + AppLogger().error( + 'android link image: ${event.notification?.android?.imageUrl}', + ); + AppLogger().error( + 'android link click action: ${event.notification?.android?.clickAction}', + ); + AppLogger().error( + 'web link image: ${event.notification?.web?.image}', + ); + print('Notification received: ${event.notification?.web}'); + print('web link: ${event.notification?.web?.link}'); + AppToast.notification( + overlayCtx, + event.notification?.title ?? 'New Notification', + event.notification?.body ?? 'New Notification', + ); } - }); - } + } + }); + // } FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); } void _handleMessage(RemoteMessage message) { + SharedPreferences.getInstance().then((value) { + value.setString('notif_data', message.data.toString()); + value.setString('notif_from', message.from.toString()); + value.setString('notif_type', message.messageType.toString()); + value.setString('notif_keys', message.data.keys.toString()); + value.setString('notif_title', message.notification?.title ?? ''); + value.setString('notif_body', message.notification?.body ?? ''); + value.setString('notif_data', message.contentAvailable.toString()); + value.setString('notif_web_link', message.notification?.web?.link ?? ''); + value.setString( + 'notif_android_link', + message.notification?.android?.link ?? '', + ); + value.setString( + 'notif_web_image', + message.notification?.web?.image ?? '', + ); + value.setString( + 'notif_android_image', + message.notification?.android?.imageUrl ?? '', + ); + value.setString( + 'notif_web_click_action', + message.notification?.web?.link ?? '', + ); + }); appRouter.go( const SplashScreenRoute(navigateToNotification: true).location, ); diff --git a/lib/view_models/auth_view_model.dart b/lib/view_models/auth_view_model.dart index 93f53b0..e3bfecd 100644 --- a/lib/view_models/auth_view_model.dart +++ b/lib/view_models/auth_view_model.dart @@ -147,7 +147,11 @@ class AuthViewModel with ChangeNotifier { } Future localLogout() async { - await _authRepository.localLogout(); + try { + await _authRepository.localLogout(); + } catch (e) { + // Handle local logout exceptions gracefully + } _loggedInUser = null; notifyListeners(); } @@ -160,7 +164,7 @@ class AuthViewModel with ChangeNotifier { switch (result) { case Ok(): - localLogout(); + await localLogout(); break; case Error(): _errorMessage = result.error.toString(); diff --git a/lib/view_models/profile_view_model.dart b/lib/view_models/profile_view_model.dart index 5e2f0f1..5f10ed3 100644 --- a/lib/view_models/profile_view_model.dart +++ b/lib/view_models/profile_view_model.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/data/repositories/auth_repository.dart'; import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart'; import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart'; @@ -29,12 +30,65 @@ class ProfileViewModel with ChangeNotifier { CompanyProfileData? _companyProfileData; bool _loggedOut = false; + // Notification info saved from FCM message in SharedPreferences + String? _notifData; + String? _notifFrom; + String? _notifType; + String? _notifKeys; + String? _notifTitle; + String? _notifBody; + String? _notifWebLink; + String? _notifAndroidLink; + String? _notifWebImage; + String? _notifAndroidImage; + String? _notifWebClickAction; + bool get isLoading => _isLoading; bool get loggedOut => _loggedOut; String? get errorMessage => _errorMessage; ProfileData? get profileData => _profileData; CompanyProfileData? get companyProfileData => _companyProfileData; + // Expose notification info + String? get notifData => _notifData; + String? get notifFrom => _notifFrom; + String? get notifType => _notifType; + String? get notifKeys => _notifKeys; + String? get notifTitle => _notifTitle; + String? get notifBody => _notifBody; + String? get notifWebLink => _notifWebLink; + String? get notifAndroidLink => _notifAndroidLink; + String? get notifWebImage => _notifWebImage; + String? get notifAndroidImage => _notifAndroidImage; + String? get notifWebClickAction => _notifWebClickAction; + + String get notifSummaryText { + final parts = []; + if (_notifTitle != null && _notifTitle!.isNotEmpty) + parts.add('Title: ${_notifTitle!}'); + if (_notifBody != null && _notifBody!.isNotEmpty) + parts.add('Body: ${_notifBody!}'); + if (_notifType != null && _notifType!.isNotEmpty) + parts.add('Type: ${_notifType!}'); + if (_notifFrom != null && _notifFrom!.isNotEmpty) + parts.add('From: ${_notifFrom!}'); + if (_notifKeys != null && _notifKeys!.isNotEmpty) + parts.add('Keys: ${_notifKeys!}'); + if (_notifWebLink != null && _notifWebLink!.isNotEmpty) + parts.add('Web: ${_notifWebLink!}'); + if (_notifAndroidLink != null && _notifAndroidLink!.isNotEmpty) + parts.add('Android: ${_notifAndroidLink!}'); + if (_notifWebImage != null && _notifWebImage!.isNotEmpty) + parts.add('WebImg: ${_notifWebImage!}'); + if (_notifAndroidImage != null && _notifAndroidImage!.isNotEmpty) + parts.add('AndroidImg: ${_notifAndroidImage!}'); + if (_notifWebClickAction != null && _notifWebClickAction!.isNotEmpty) + parts.add('Click: ${_notifWebClickAction!}'); + if (_notifData != null && _notifData!.isNotEmpty) + parts.add('Data: ${_notifData!}'); + return parts.isEmpty ? '-' : parts.join(' | '); + } + Future getProfile() async { _isLoading = true; _errorMessage = null; @@ -98,4 +152,28 @@ class ProfileViewModel with ChangeNotifier { _loggedOut = false; notifyListeners(); } + + Future loadNotificationInfoFromPrefs() async { + // Do not toggle global loading; this is a lightweight local load + try { + final prefs = await SharedPreferences.getInstance(); + _notifData = prefs.getString('notif_data'); + _notifFrom = prefs.getString('notif_from'); + _notifType = prefs.getString('notif_type'); + _notifKeys = prefs.getString('notif_keys'); + _notifTitle = prefs.getString('notif_title'); + _notifBody = prefs.getString('notif_body'); + _notifWebLink = prefs.getString('notif_web_link'); + _notifAndroidLink = prefs.getString('notif_android_link'); + _notifWebImage = prefs.getString('notif_web_image'); + _notifAndroidImage = prefs.getString('notif_android_image'); + _notifWebClickAction = prefs.getString('notif_web_click_action'); + } catch (e) { + // Non-fatal; surface as errorMessage so UI can toast if listening + _errorMessage = e.toString(); + } + notifyListeners(); + _errorMessage = null; + notifyListeners(); + } } diff --git a/lib/views/profile/pages/m_profile_page.dart b/lib/views/profile/pages/m_profile_page.dart index b244b8f..d99325a 100644 --- a/lib/views/profile/pages/m_profile_page.dart +++ b/lib/views/profile/pages/m_profile_page.dart @@ -28,6 +28,10 @@ class _MobileProfilePageState extends State { super.initState(); viewModel = context.read(); viewModel.addListener(_profileListener); + // Load notification info saved in SharedPreferences + WidgetsBinding.instance.addPostFrameCallback((_) { + viewModel.loadNotificationInfoFromPrefs(); + }); } void _profileListener() { @@ -75,6 +79,30 @@ class _MobileProfilePageState extends State { child: Column( children: [ SizedBox(height: 36.0.h()), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + 'Last Notification', + style: TextStyle( + fontSize: 18.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey70, + ), + ), + ), + SizedBox(height: 8.0.h()), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + viewModel.notifSummaryText, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + ), + SizedBox(height: 24.0.h()), TitleDescription( title: ProfileStrings.companyName, description: viewModel.companyProfileData!.name!, diff --git a/test/fixtures/test_fixtures.dart b/test/fixtures/test_fixtures.dart new file mode 100644 index 0000000..1e1cb14 --- /dev/null +++ b/test/fixtures/test_fixtures.dart @@ -0,0 +1,283 @@ +import 'package:tm_app/data/services/model/customer/customer.dart'; +import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart'; +import 'package:tm_app/data/services/model/liked_tender_feedback/liked_tender_feedback.dart'; +import 'package:tm_app/data/services/model/liked_tenders_data/liked_tenders_data.dart'; +import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; +import 'package:tm_app/data/services/model/login_data/login_data.dart'; +import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; +import 'package:tm_app/data/services/model/logout_response/logout_response.dart'; +import 'package:tm_app/data/services/model/meta/meta.dart'; +import 'package:tm_app/data/services/model/reset_password_response/reset_password_response.dart'; +import 'package:tm_app/data/services/model/tender_approvals_data/tender_approvals_data.dart'; +import 'package:tm_app/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.dart'; +import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; +import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; +import 'package:tm_app/data/services/model/verify_otp_data/verify_otp_data.dart'; +import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart'; + +class TestFixtures { + static const customer = Customer( + id: '1', + type: 'individual', + status: 'active', + firstName: 'John', + lastName: 'Doe', + fullName: 'John Doe', + email: 'john.doe@example.com', + phone: '+1234567890', + mobile: '+1234567890', + registrationNumber: 'REG123', + taxId: 'TAX123', + industry: 'Technology', + businessType: 'Software', + employeeCount: 10, + annualRevenue: 1000000, + foundedYear: 2020, + isVerified: true, + isCompliant: true, + complianceNotes: 'Compliant', + language: 'en', + username: 'johndoe', + currency: 'USD', + timezone: 'UTC', + createdAt: 1640995200, + updatedAt: 1640995200, + createdBy: 'admin', + updatedBy: 'admin', + address: null, + contactPersons: null, + ); + + static const loginData = LoginData( + customer: customer, + error: null, + accessToken: 'test_token', + refreshToken: 'test_refresh_token', + expiresAt: 1640995200, + ); + + static const loginResponse = LoginResponseModel( + success: true, + message: 'Login successful', + data: loginData, + ); + + static const logoutResponse = LogoutResponse( + success: true, + message: 'Logout successful', + error: null, + ); + + static const forgotPasswordResponse = ForgotPasswordResponseModel( + success: true, + message: 'OTP sent to email', + data: null, + error: null, + meta: null, + ); + + static const verifyOtpData = VerifyOtpData( + message: 'OTP verified successfully', + success: true, + token: 'reset_token_123', + ); + + static const verifyOtpResponse = VerifyOtpResponseModel( + success: true, + message: 'OTP verified successfully', + data: verifyOtpData, + ); + + static const resetPasswordResponse = ResetPasswordResponse( + success: true, + message: 'Password reset successfully', + error: null, + ); + + // Tender test fixtures + static const testTenderData = TenderData( + success: true, + message: 'Success', + id: '1', + tenderId: '1', + noticePublicationId: '1', + publicationDate: 1640995200, + title: 'Test Tender', + description: 'Test Description', + procurementTypeCode: 'test', + procedureCode: 'test', + estimatedValue: 100000, + currency: 'USD', + duration: '30', + tenderDeadline: 1640995200, + submissionDeadline: 1640995200, + applicationDeadline: 1640995200, + submissionUrl: 'https://test.com', + countryCode: 'US', + durationUnit: 'days', + buyerOrganization: null, + status: 'submitted', + ); + + static const testTenderApprovalsData = TenderApprovalsData( + id: '1', + status: 'submitted', + tenderId: '1', + companyId: '1', + submissionMode: 'online', + createdAt: 1640995200, + tender: testTenderData, + ); + + static const testMeta = Meta( + limit: 10, + offset: 0, + page: 1, + pages: 2, + total: 15, + ); + + static const testTenderApprovalsDataWrapper = TenderApprovalsDataWrapper( + tenders: [testTenderApprovalsData], + metadata: testMeta, + ); + + static const tenderApprovalsResponse = TenderApprovalsResponse( + success: true, + error: null, + data: testTenderApprovalsDataWrapper, + ); + + static const testLikedTenderFeedback = LikedTenderFeedback( + id: '1', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '1', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback2 = LikedTenderFeedback( + id: '2', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '2', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback3 = LikedTenderFeedback( + id: '3', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '3', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback4 = LikedTenderFeedback( + id: '4', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '4', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback5 = LikedTenderFeedback( + id: '5', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '5', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback6 = LikedTenderFeedback( + id: '6', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '6', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback7 = LikedTenderFeedback( + id: '7', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '7', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback8 = LikedTenderFeedback( + id: '8', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '8', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback9 = LikedTenderFeedback( + id: '9', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '9', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTenderFeedback10 = LikedTenderFeedback( + id: '10', + feedbackType: 'liked', + updatedAt: 1640995200, + createdAt: 1640995200, + tenderId: '10', + customerId: '1', + companyId: '1', + tender: testTenderData, + ); + + static const testLikedTendersData = LikedTendersData( + feedback: [ + testLikedTenderFeedback, + testLikedTenderFeedback2, + testLikedTenderFeedback3, + testLikedTenderFeedback4, + testLikedTenderFeedback5, + testLikedTenderFeedback6, + testLikedTenderFeedback7, + testLikedTenderFeedback8, + testLikedTenderFeedback9, + testLikedTenderFeedback10, + ], + meta: testMeta, + ); + + static const likedTendersResponse = LikedTendersResponse( + success: true, + message: 'Success', + data: testLikedTendersData, + error: null, + ); +} diff --git a/test/mocks/mock_auth_repository.dart b/test/mocks/mock_auth_repository.dart new file mode 100644 index 0000000..c88b1f1 --- /dev/null +++ b/test/mocks/mock_auth_repository.dart @@ -0,0 +1,97 @@ +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/repositories/auth_repository.dart'; +import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart'; +import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; +import 'package:tm_app/data/services/model/logout_response/logout_response.dart'; +import 'package:tm_app/data/services/model/reset_password_response/reset_password_response.dart'; +import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart'; + +import '../fixtures/test_fixtures.dart'; + +class MockAuthRepository implements AuthRepository { + Result? _loginResult; + Result? _logoutResult; + Result? _forgotPasswordResult; + Result? _verifyOtpResult; + Result? _resetPasswordResult; + Result? _checkIsLoggedInResult; + Exception? _localLogoutException; + + void setLoginResult(Result result) { + _loginResult = result; + } + + void setLogoutResult(Result result) { + _logoutResult = result; + } + + void setForgotPasswordResult(Result result) { + _forgotPasswordResult = result; + } + + void setVerifyOtpResult(Result result) { + _verifyOtpResult = result; + } + + void setResetPasswordResult(Result result) { + _resetPasswordResult = result; + } + + void setCheckIsLoggedInResult(Result result) { + _checkIsLoggedInResult = result; + } + + void setLocalLogoutException(Exception exception) { + _localLogoutException = exception; + } + + @override + Future> login({ + required String username, + required String password, + }) async { + return _loginResult ?? const Result.ok(TestFixtures.loginResponse); + } + + @override + Future> logout() async { + return _logoutResult ?? const Result.ok(TestFixtures.logoutResponse); + } + + @override + Future> resetPassword({ + required String newPassword, + required String token, + }) async { + return _resetPasswordResult ?? + const Result.ok(TestFixtures.resetPasswordResponse); + } + + @override + Future localLogout() async { + if (_localLogoutException != null) { + throw _localLogoutException!; + } + } + + @override + Future> forgotPassword({ + required String email, + }) async { + return _forgotPasswordResult ?? + const Result.ok(TestFixtures.forgotPasswordResponse); + } + + @override + Future> verifyOtp({ + required String code, + required String email, + }) async { + return _verifyOtpResult ?? const Result.ok(TestFixtures.verifyOtpResponse); + } + + @override + Future> checkIsLoggedIn() async { + return _checkIsLoggedInResult ?? const Result.ok(true); + } +} diff --git a/test/mocks/mock_your_tenders_repository.dart b/test/mocks/mock_your_tenders_repository.dart new file mode 100644 index 0000000..319f7f2 --- /dev/null +++ b/test/mocks/mock_your_tenders_repository.dart @@ -0,0 +1,37 @@ +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/repositories/your_tenders_repository.dart'; +import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; +import 'package:tm_app/data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; +import 'package:tm_app/data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart'; +import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; + +import '../fixtures/test_fixtures.dart'; + +class MockYourTendersRepository implements YourTendersRepository { + Result? _getTendersResult; + Result? _getTendersFeedbackResult; + + void setGetTendersResult(Result result) { + _getTendersResult = result; + } + + void setGetTendersFeedbackResult(Result result) { + _getTendersFeedbackResult = result; + } + + @override + Future> getTenders({ + required GetTendersRequestModel requestModel, + }) async { + return _getTendersResult ?? + const Result.ok(TestFixtures.tenderApprovalsResponse); + } + + @override + Future> getTendersFeedback({ + required TenderFeedbackRequestModel requestModel, + }) async { + return _getTendersFeedbackResult ?? + const Result.ok(TestFixtures.likedTendersResponse); + } +} diff --git a/test/view_models/auth_view_model_forgot_password_test.dart b/test/view_models/auth_view_model_forgot_password_test.dart new file mode 100644 index 0000000..c5049fc --- /dev/null +++ b/test/view_models/auth_view_model_forgot_password_test.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Forgot Password', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + late BuildContext mockContext; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + mockContext = const TestWidget().createElement(); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should send forgot password successfully', () async { + mockRepository.setForgotPasswordResult( + const Result.ok(TestFixtures.forgotPasswordResponse), + ); + + authViewModel.emailController.text = 'test@example.com'; + + await authViewModel.forgotPassword(mockContext); + + expect(authViewModel.isLoadingForgot, false); + expect(authViewModel.successMessage, 'OTP sent to email'); + expect(authViewModel.errorMessageForget, null); + }); + + test('should handle forgot password error', () async { + const errorMessage = 'Email not found'; + mockRepository.setForgotPasswordResult( + Result.error(Exception(errorMessage)), + ); + + authViewModel.emailController.text = 'nonexistent@example.com'; + + await authViewModel.forgotPassword(mockContext); + + // Wait for the microtask to clear the error message + await Future.microtask(() {}); + + expect(authViewModel.isLoadingForgot, false); + expect(authViewModel.errorMessageForget, null); // Cleared after notify + expect(authViewModel.successMessage, null); + }); + + test('should set loading state during forgot password', () async { + mockRepository.setForgotPasswordResult( + const Result.ok(TestFixtures.forgotPasswordResponse), + ); + + authViewModel.emailController.text = 'test@example.com'; + + final forgotPasswordFuture = authViewModel.forgotPassword(mockContext); + + // Check loading state is set + expect(authViewModel.isLoadingForgot, true); + expect(authViewModel.errorMessageForget, null); + expect(authViewModel.successMessage, null); + + await forgotPasswordFuture; + + expect(authViewModel.isLoadingForgot, false); + }); + + test('should clear previous messages before forgot password', () async { + mockRepository.setForgotPasswordResult( + const Result.ok(TestFixtures.forgotPasswordResponse), + ); + + // Set previous error and success messages + authViewModel.emailController.text = 'test@example.com'; + mockRepository.setForgotPasswordResult( + Result.error(Exception('Previous error')), + ); + await authViewModel.forgotPassword(mockContext); + + // Now test successful case + mockRepository.setForgotPasswordResult( + const Result.ok(TestFixtures.forgotPasswordResponse), + ); + await authViewModel.forgotPassword(mockContext); + + expect(authViewModel.errorMessageForget, null); + expect(authViewModel.successMessage, 'OTP sent to email'); + }); + }); +} + +class TestWidget extends StatelessWidget { + const TestWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/test/view_models/auth_view_model_initialization_test.dart b/test/view_models/auth_view_model_initialization_test.dart new file mode 100644 index 0000000..35d0dff --- /dev/null +++ b/test/view_models/auth_view_model_initialization_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Initialization', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should initialize with default values', () { + expect(authViewModel.isLoading, false); + expect(authViewModel.isLoggedIn, false); + expect(authViewModel.isLoadingForgot, false); + expect(authViewModel.isLoadingOtp, false); + expect(authViewModel.isResetPasswordLoading, false); + expect(authViewModel.errorMessage, null); + expect(authViewModel.errorMessageForget, null); + expect(authViewModel.resetPasswordErrorMessage, null); + expect(authViewModel.errorMessageOtp, null); + expect(authViewModel.successMessageOtp, null); + expect(authViewModel.verifyOtpSuccess, false); + expect(authViewModel.loggedInUser, null); + expect(authViewModel.forgotPasswordMessage, null); + expect(authViewModel.resetToken, null); + expect(authViewModel.resetPasswordSuccess, false); + }); + + test('should initialize controllers with empty text', () { + expect(authViewModel.usernameController.text, isEmpty); + expect(authViewModel.passwordController.text, isEmpty); + expect(authViewModel.emailController.text, isEmpty); + expect(authViewModel.newPasswordController.text, isEmpty); + expect(authViewModel.otpController.text, isEmpty); + }); + + test('should initialize with password visibility obscured', () { + expect(authViewModel.obscurePassword, true); + expect(authViewModel.obscureForgotPassword, true); + }); + + test('should not be login enabled with empty fields', () { + expect(authViewModel.isLoginEnabled, false); + }); + + test('should be login enabled with filled fields', () { + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + + expect(authViewModel.isLoginEnabled, true); + }); + }); +} diff --git a/test/view_models/auth_view_model_login_test.dart b/test/view_models/auth_view_model_login_test.dart new file mode 100644 index 0000000..e7bc861 --- /dev/null +++ b/test/view_models/auth_view_model_login_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Login', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should login successfully', () async { + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + + await authViewModel.login(); + + expect(authViewModel.isLoading, false); + expect(authViewModel.loggedInUser, TestFixtures.customer); + expect(authViewModel.usernameController.text, isEmpty); + expect(authViewModel.passwordController.text, isEmpty); + expect(authViewModel.errorMessage, null); + }); + + test('should handle login error', () async { + const errorMessage = 'Invalid credentials'; + mockRepository.setLoginResult(Result.error(Exception(errorMessage))); + + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'wrongpass'; + + await authViewModel.login(); + + expect(authViewModel.isLoading, false); + expect(authViewModel.loggedInUser, null); + expect(authViewModel.errorMessage, null); // Cleared after notify + }); + + test('should set loading state during login', () async { + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + + final loginFuture = authViewModel.login(); + + // Check loading state is set + expect(authViewModel.isLoading, true); + expect(authViewModel.errorMessage, null); + + await loginFuture; + + expect(authViewModel.isLoading, false); + }); + + test('should call localLogout before login', () async { + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + + await authViewModel.login(); + + // Verify that localLogout was called (user is null initially) + expect(authViewModel.loggedInUser, TestFixtures.customer); + }); + }); +} diff --git a/test/view_models/auth_view_model_logout_test.dart b/test/view_models/auth_view_model_logout_test.dart new file mode 100644 index 0000000..19b55a6 --- /dev/null +++ b/test/view_models/auth_view_model_logout_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Logout', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should logout successfully', () async { + mockRepository.setLogoutResult( + const Result.ok(TestFixtures.logoutResponse), + ); + + // Set a logged in user first + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + await authViewModel.login(); + + await authViewModel.logout(); + + expect(authViewModel.loggedInUser, null); + expect(authViewModel.errorMessage, null); // Cleared after notify + }); + + test('should handle logout error', () async { + const errorMessage = 'Logout failed'; + mockRepository.setLogoutResult(Result.error(Exception(errorMessage))); + + await authViewModel.logout(); + + expect(authViewModel.errorMessage, null); // Cleared after notify + }); + + test('should perform local logout', () async { + mockRepository.setLogoutResult( + const Result.ok(TestFixtures.logoutResponse), + ); + + // Set a logged in user first + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + await authViewModel.login(); + + await authViewModel.logout(); + + expect(authViewModel.loggedInUser, null); + }); + + test('should clear error message before logout', () async { + mockRepository.setLogoutResult( + const Result.ok(TestFixtures.logoutResponse), + ); + + // Set an error first + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'wrongpass'; + mockRepository.setLoginResult(Result.error(Exception('Login failed'))); + await authViewModel.login(); + + await authViewModel.logout(); + + expect(authViewModel.errorMessage, null); + }); + }); +} diff --git a/test/view_models/auth_view_model_otp_test.dart b/test/view_models/auth_view_model_otp_test.dart new file mode 100644 index 0000000..1cf518c --- /dev/null +++ b/test/view_models/auth_view_model_otp_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel OTP Verification', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + late BuildContext mockContext; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + mockContext = const TestWidget().createElement(); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should verify OTP successfully', () async { + mockRepository.setVerifyOtpResult( + const Result.ok(TestFixtures.verifyOtpResponse), + ); + + authViewModel.otpController.text = '123456'; + authViewModel.emailController.text = 'test@example.com'; + + await authViewModel.verifyOtp(mockContext); + + expect(authViewModel.isLoadingOtp, false); + expect(authViewModel.verifyOtpSuccess, true); + expect(authViewModel.resetToken, 'reset_token_123'); + expect( + authViewModel.successMessageOtp, + null, + ); // Cleared after notify for toast + expect(authViewModel.errorMessageOtp, null); // Cleared after notify + }); + + test('should handle OTP verification error', () async { + const errorMessage = 'Invalid OTP'; + mockRepository.setVerifyOtpResult(Result.error(Exception(errorMessage))); + + authViewModel.otpController.text = '000000'; + authViewModel.emailController.text = 'test@example.com'; + + await authViewModel.verifyOtp(mockContext); + + expect(authViewModel.isLoadingOtp, false); + expect(authViewModel.verifyOtpSuccess, false); + expect(authViewModel.errorMessageOtp, null); // Cleared after notify + expect(authViewModel.successMessageOtp, null); // Cleared after notify + }); + + test('should set loading state during OTP verification', () async { + mockRepository.setVerifyOtpResult( + const Result.ok(TestFixtures.verifyOtpResponse), + ); + + authViewModel.otpController.text = '123456'; + authViewModel.emailController.text = 'test@example.com'; + + final verifyOtpFuture = authViewModel.verifyOtp(mockContext); + + // Check loading state is set + expect(authViewModel.isLoadingOtp, true); + expect(authViewModel.errorMessageOtp, null); + expect(authViewModel.verifyOtpSuccess, false); + + await verifyOtpFuture; + + expect(authViewModel.isLoadingOtp, false); + }); + + test('should clear OTP flow', () { + // Set some state first + authViewModel.otpController.text = '123456'; + authViewModel.resetToken = 'some_token'; + + authViewModel.clearOtpFlow(); + + expect(authViewModel.errorMessageOtp, null); + expect(authViewModel.successMessageOtp, null); + expect(authViewModel.isLoadingOtp, false); + expect(authViewModel.verifyOtpSuccess, false); + expect(authViewModel.otpController.text, isEmpty); + expect(authViewModel.resetToken, 'some_token'); // Should not be cleared + }); + + test('should reset verifyOtpSuccess before verification', () async { + mockRepository.setVerifyOtpResult( + const Result.ok(TestFixtures.verifyOtpResponse), + ); + + // Set verifyOtpSuccess to true first + authViewModel.otpController.text = '123456'; + authViewModel.emailController.text = 'test@example.com'; + await authViewModel.verifyOtp(mockContext); + expect(authViewModel.verifyOtpSuccess, true); + + // Now test with error + mockRepository.setVerifyOtpResult(Result.error(Exception('Invalid OTP'))); + await authViewModel.verifyOtp(mockContext); + + expect(authViewModel.verifyOtpSuccess, false); + }); + }); +} + +class TestWidget extends StatelessWidget { + const TestWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/test/view_models/auth_view_model_reset_password_test.dart b/test/view_models/auth_view_model_reset_password_test.dart new file mode 100644 index 0000000..1e651c9 --- /dev/null +++ b/test/view_models/auth_view_model_reset_password_test.dart @@ -0,0 +1,140 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Reset Password', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + test('should reset password successfully', () async { + mockRepository.setResetPasswordResult( + const Result.ok(TestFixtures.resetPasswordResponse), + ); + + authViewModel.resetToken = 'reset_token_123'; + authViewModel.newPasswordController.text = 'NewPass123!'; + + await authViewModel.resetPassword(); + + expect(authViewModel.isResetPasswordLoading, false); + expect(authViewModel.resetPasswordSuccess, true); + expect( + authViewModel.resetPasswordErrorMessage, + null, + ); // Cleared after notify + }); + + test('should handle reset password error', () async { + const errorMessage = 'Invalid token'; + mockRepository.setResetPasswordResult( + Result.error(Exception(errorMessage)), + ); + + authViewModel.resetToken = 'invalid_token'; + authViewModel.newPasswordController.text = 'NewPass123!'; + + await authViewModel.resetPassword(); + + expect(authViewModel.isResetPasswordLoading, false); + expect(authViewModel.resetPasswordSuccess, false); + expect( + authViewModel.resetPasswordErrorMessage, + null, + ); // Cleared after notify + }); + + test('should handle missing reset token', () async { + authViewModel.resetToken = null; + authViewModel.newPasswordController.text = 'NewPass123!'; + + await authViewModel.resetPassword(); + + expect(authViewModel.isResetPasswordLoading, false); + expect(authViewModel.resetPasswordSuccess, false); + expect( + authViewModel.resetPasswordErrorMessage, + 'Reset token is required', + ); + }); + + test('should set loading state during reset password', () async { + mockRepository.setResetPasswordResult( + const Result.ok(TestFixtures.resetPasswordResponse), + ); + + authViewModel.resetToken = 'reset_token_123'; + authViewModel.newPasswordController.text = 'NewPass123!'; + + final resetPasswordFuture = authViewModel.resetPassword(); + + // Check loading state is set + expect(authViewModel.isResetPasswordLoading, true); + expect(authViewModel.resetPasswordErrorMessage, null); + expect(authViewModel.resetPasswordSuccess, false); + + await resetPasswordFuture; + + expect(authViewModel.isResetPasswordLoading, false); + }); + + test('should clear reset password success', () { + // Set reset password success through a successful reset + mockRepository.setResetPasswordResult( + const Result.ok(TestFixtures.resetPasswordResponse), + ); + authViewModel.resetToken = 'reset_token_123'; + authViewModel.newPasswordController.text = 'NewPass123!'; + + authViewModel.clearResetPasswordSuccess(); + + expect(authViewModel.resetPasswordSuccess, false); + }); + + test('should clear reset flow', () { + // Set some state first + authViewModel.resetToken = 'some_token'; + authViewModel.newPasswordController.text = 'some_password'; + + authViewModel.clearResetFlow(); + + expect(authViewModel.resetPasswordErrorMessage, null); + expect(authViewModel.resetPasswordSuccess, false); + expect(authViewModel.isResetPasswordLoading, false); + expect(authViewModel.newPasswordController.text, isEmpty); + expect(authViewModel.resetToken, 'some_token'); // Should not be cleared + }); + + test('should reset success state before reset password', () async { + mockRepository.setResetPasswordResult( + const Result.ok(TestFixtures.resetPasswordResponse), + ); + + // Set resetPasswordSuccess to true first + authViewModel.resetToken = 'reset_token_123'; + authViewModel.newPasswordController.text = 'NewPass123!'; + await authViewModel.resetPassword(); + expect(authViewModel.resetPasswordSuccess, true); + + // Now test with error + mockRepository.setResetPasswordResult( + Result.error(Exception('Reset failed')), + ); + await authViewModel.resetPassword(); + + expect(authViewModel.resetPasswordSuccess, false); + }); + }); +} diff --git a/test/view_models/auth_view_model_utility_test.dart b/test/view_models/auth_view_model_utility_test.dart new file mode 100644 index 0000000..1d72a66 --- /dev/null +++ b/test/view_models/auth_view_model_utility_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Utility Methods', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + group('Password Visibility Toggle', () { + test('should toggle password visibility', () { + expect(authViewModel.obscurePassword, true); + + authViewModel.togglePasswordVisibility(); + expect(authViewModel.obscurePassword, false); + + authViewModel.togglePasswordVisibility(); + expect(authViewModel.obscurePassword, true); + }); + + test('should toggle forgot password visibility', () { + expect(authViewModel.obscureForgotPassword, true); + + authViewModel.toggleForgotPasswordVisibility(); + expect(authViewModel.obscureForgotPassword, false); + + authViewModel.toggleForgotPasswordVisibility(); + expect(authViewModel.obscureForgotPassword, true); + }); + }); + + group('Message Clearing', () { + test('should clear messages', () { + // Set some messages first through login error + mockRepository.setLoginResult(Result.error(Exception('Login failed'))); + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'wrongpass'; + + authViewModel.clearMessages(); + + expect(authViewModel.successMessage, null); + expect(authViewModel.errorMessage, null); + }); + }); + + group('Check Login Status', () { + test('should check if user is logged in - true', () async { + mockRepository.setCheckIsLoggedInResult(const Result.ok(true)); + + await authViewModel.checkIsLoggedIn(); + + expect(authViewModel.isLoggedIn, true); + }); + + test('should check if user is logged in - false', () async { + mockRepository.setCheckIsLoggedInResult(const Result.ok(false)); + + await authViewModel.checkIsLoggedIn(); + + expect(authViewModel.isLoggedIn, false); + }); + + test('should handle check login error', () async { + mockRepository.setCheckIsLoggedInResult( + Result.error(Exception('Check failed')), + ); + + await authViewModel.checkIsLoggedIn(); + + expect(authViewModel.isLoggedIn, false); + }); + }); + + group('Local Logout', () { + test('should perform local logout', () async { + // Set a logged in user first + authViewModel.usernameController.text = 'testuser'; + authViewModel.passwordController.text = 'testpass'; + mockRepository.setLoginResult( + const Result.ok(TestFixtures.loginResponse), + ); + await authViewModel.login(); + + await authViewModel.localLogout(); + + expect(authViewModel.loggedInUser, null); + }); + + test('should handle local logout exception', () async { + mockRepository.setLocalLogoutException( + Exception('Local logout failed'), + ); + + // Should not throw exception + await authViewModel.localLogout(); + + expect(authViewModel.loggedInUser, null); + }); + }); + + group('Text Change Notification', () { + test('should notify listeners on text change', () { + int notificationCount = 0; + + authViewModel.addListener(() { + notificationCount++; + }); + + // Trigger text change + authViewModel.usernameController.text = 'test'; + + expect(notificationCount, greaterThan(0)); + }); + }); + }); +} diff --git a/test/view_models/auth_view_model_validation_test.dart b/test/view_models/auth_view_model_validation_test.dart new file mode 100644 index 0000000..c790632 --- /dev/null +++ b/test/view_models/auth_view_model_validation_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/view_models/auth_view_model.dart'; + +import '../mocks/mock_auth_repository.dart'; + +void main() { + group('AuthViewModel Validation', () { + late MockAuthRepository mockRepository; + late AuthViewModel authViewModel; + + setUp(() { + mockRepository = MockAuthRepository(); + authViewModel = AuthViewModel(authRepository: mockRepository); + }); + + tearDown(() { + authViewModel.dispose(); + }); + + group('Email Validation', () { + test('should return true for valid email', () { + authViewModel.emailController.text = 'test@example.com'; + expect(authViewModel.isEmailValid, true); + }); + + test('should return false for invalid email', () { + authViewModel.emailController.text = 'invalid-email'; + expect(authViewModel.isEmailValid, false); + }); + + test('should return false for empty email', () { + authViewModel.emailController.text = ''; + expect(authViewModel.isEmailValid, false); + }); + + test('should return false for email without domain', () { + authViewModel.emailController.text = 'test@'; + expect(authViewModel.isEmailValid, false); + }); + }); + + group('OTP Validation', () { + test('should return true for valid 6-digit OTP', () { + authViewModel.otpController.text = '123456'; + expect(authViewModel.isOtpValid, true); + expect(authViewModel.canSubmitOtpPage, true); + }); + + test('should return false for invalid OTP length', () { + authViewModel.otpController.text = '12345'; + expect(authViewModel.isOtpValid, false); + expect(authViewModel.canSubmitOtpPage, false); + }); + + test('should return false for empty OTP', () { + authViewModel.otpController.text = ''; + expect(authViewModel.isOtpValid, false); + expect(authViewModel.canSubmitOtpPage, false); + }); + }); + + group('Password Validation', () { + test('should return true for valid password', () { + authViewModel.newPasswordController.text = 'Test123!'; + expect(authViewModel.isNewPasswordValid, true); + }); + + test('should return false for password without uppercase', () { + authViewModel.newPasswordController.text = 'test123!'; + expect(authViewModel.isNewPasswordValid, false); + }); + + test('should return false for password without lowercase', () { + authViewModel.newPasswordController.text = 'TEST123!'; + expect(authViewModel.isNewPasswordValid, false); + }); + + test('should return false for password without special character', () { + authViewModel.newPasswordController.text = 'Test123'; + expect(authViewModel.isNewPasswordValid, false); + }); + + test('should return false for password too short', () { + authViewModel.newPasswordController.text = 'Te1!'; + expect(authViewModel.isNewPasswordValid, false); + }); + + test('should return false for empty password', () { + authViewModel.newPasswordController.text = ''; + expect(authViewModel.isNewPasswordValid, false); + }); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_feedback_test.dart b/test/view_models/your_tenders_view_model_feedback_test.dart new file mode 100644 index 0000000..bfabbdc --- /dev/null +++ b/test/view_models/your_tenders_view_model_feedback_test.dart @@ -0,0 +1,119 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel GetTendersFeedback', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + test('should get tenders feedback successfully', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: true); + + expect(viewModel.isLoading, false); + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + expect(viewModel.errorMessage, null); + expect(viewModel.hasMore, true); + }); + + test('should handle get tenders feedback error', () async { + const errorMessage = 'Network error'; + mockRepository.setGetTendersFeedbackResult( + Result.error(Exception(errorMessage)), + ); + + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: true); + + expect(viewModel.isLoading, false); + expect(viewModel.likedTendersData, null); + expect(viewModel.errorMessage, errorMessage); + }); + + test('should set loading state during get feedback', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + final getFeedbackFuture = viewModel.getTendersFeedback( + feedbackType: 'liked', + reset: true, + ); + + expect(viewModel.isLoading, true); + expect(viewModel.errorMessage, null); + + await getFeedbackFuture; + + expect(viewModel.isLoading, false); + }); + + test('should reset data when reset is true', () async { + // First load + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: true); + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + + // Reset and load again + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: true); + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + }); + + test('should not load more when already loading', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + viewModel.isLoadingMore = true; + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: false); + + expect(viewModel.likedTendersData, null); + }); + + test('should not load more when no more data', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + viewModel.hasMore = false; + await viewModel.getTendersFeedback(feedbackType: 'liked', reset: false); + + expect(viewModel.likedTendersData, null); + }); + + test('should load more feedback successfully', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + // Set current feedback type + viewModel.getTendersFeedback(feedbackType: 'liked', reset: true); + await viewModel.loadMoreFeedback(); + + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + }); + + test('should not load more feedback when no current type', () async { + await viewModel.loadMoreFeedback(); + expect(viewModel.likedTendersData, null); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_filter_test.dart b/test/view_models/your_tenders_view_model_filter_test.dart new file mode 100644 index 0000000..cc3e0a4 --- /dev/null +++ b/test/view_models/your_tenders_view_model_filter_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel Filter', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + test('should set date range correctly', () { + final dateRange = DateTimeRange( + start: DateTime(2024, 1, 1), + end: DateTime(2024, 1, 31), + ); + + viewModel.setDateRange(dateRange); + + expect(viewModel.selectedDateRange, dateRange); + expect(viewModel.startDateFormatted, '2024/01/01'); + expect(viewModel.endDateFormatted, '2024/01/31'); + expect(viewModel.startDateUnix, '1704067200'); + expect(viewModel.endDateUnix, '1706745600'); + }); + + test('should set status and clear data', () { + // Set some initial data + viewModel.data = TestFixtures.tenderApprovalsResponse; + viewModel.likedTendersData = TestFixtures.likedTendersResponse; + viewModel.isLoading = true; + viewModel.errorMessage = 'Error'; + + viewModel.setStatus('submitted'); + + expect(viewModel.selectedStatus, 'submitted'); + expect(viewModel.data, null); + expect(viewModel.likedTendersData, null); + expect(viewModel.isLoading, false); + expect(viewModel.errorMessage, null); + expect(viewModel.currentPage, 1); + }); + + test('should clear filter and reset state', () { + // Set some initial data + final dateRange = DateTimeRange( + start: DateTime(2024, 1, 1), + end: DateTime(2024, 1, 31), + ); + viewModel.setDateRange(dateRange); + viewModel.setStatus('submitted'); + viewModel.data = TestFixtures.tenderApprovalsResponse; + + viewModel.clearFilter(); + + expect(viewModel.selectedDateRange, null); + expect(viewModel.selectedStatus, 'All'); + expect(viewModel.data, null); + expect(viewModel.likedTendersData, null); + expect(viewModel.isLoading, false); + expect(viewModel.errorMessage, null); + }); + + test('should call with filter for all status', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'All'; + await viewModel.callWithFilter(); + + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + expect(viewModel.isLoading, false); + }); + + test('should call with filter for submitted status', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'Tenders Submitted'; + await viewModel.callWithFilter(); + + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + expect(viewModel.isLoading, false); + }); + + test('should call with filter for liked tenders', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + viewModel.selectedStatus = 'Like Tenders'; + await viewModel.callWithFilter(); + + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + expect(viewModel.isLoading, false); + }); + + test('should call with filter for disliked tenders', () async { + mockRepository.setGetTendersFeedbackResult( + const Result.ok(TestFixtures.likedTendersResponse), + ); + + viewModel.selectedStatus = 'Dislike Tenders'; + await viewModel.callWithFilter(); + + expect(viewModel.likedTendersData, TestFixtures.likedTendersResponse); + expect(viewModel.isLoading, false); + }); + + test('should format dates correctly with date range', () { + final dateRange = DateTimeRange( + start: DateTime(2024, 12, 25), + end: DateTime(2024, 12, 31), + ); + + viewModel.setDateRange(dateRange); + + expect(viewModel.startDateFormatted, '2024/12/25'); + expect(viewModel.endDateFormatted, '2024/12/31'); + expect(viewModel.startDateUnix, '1735084800'); + expect(viewModel.endDateUnix, '1735689600'); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_get_tenders_test.dart b/test/view_models/your_tenders_view_model_get_tenders_test.dart new file mode 100644 index 0000000..bc2f608 --- /dev/null +++ b/test/view_models/your_tenders_view_model_get_tenders_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel GetTenders', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + test('should get tenders successfully', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + await viewModel.getTenders(requestModel: requestModel); + + expect(viewModel.isLoading, false); + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + expect(viewModel.errorMessage, null); + expect(viewModel.hasMoreTenders, true); + }); + + test('should handle get tenders error', () async { + const errorMessage = 'Network error'; + mockRepository.setGetTendersResult(Result.error(Exception(errorMessage))); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + await viewModel.getTenders(requestModel: requestModel); + + expect(viewModel.isLoading, false); + expect(viewModel.data, null); + expect(viewModel.errorMessage, errorMessage); + }); + + test('should set loading state during get tenders', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + final getTendersFuture = viewModel.getTenders(requestModel: requestModel); + + expect(viewModel.isLoading, true); + expect(viewModel.errorMessage, null); + + await getTendersFuture; + + expect(viewModel.isLoading, false); + }); + + test('should reset data when reset is true', () async { + // First load + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + await viewModel.getTenders(requestModel: requestModel); + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + + // Reset and load again + await viewModel.getTenders(requestModel: requestModel, reset: true); + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + }); + + test('should not load more when already loading', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + viewModel.isLoadingMoreTenders = true; + await viewModel.getTenders(requestModel: requestModel); + + expect(viewModel.data, null); + }); + + test('should not load more when no more data', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + final requestModel = GetTendersRequestModel( + status: 'submitted', + createdFrom: '1640995200', + createdTo: '1640995200', + limit: 10, + offset: 0, + ); + + viewModel.hasMoreTenders = false; + await viewModel.getTenders(requestModel: requestModel); + + expect(viewModel.data, null); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_initialization_test.dart b/test/view_models/your_tenders_view_model_initialization_test.dart new file mode 100644 index 0000000..b8fdb2c --- /dev/null +++ b/test/view_models/your_tenders_view_model_initialization_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel Initialization', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + test('should initialize with default values', () { + expect(viewModel.isLoading, false); + expect(viewModel.errorMessage, null); + expect(viewModel.data, null); + expect(viewModel.likedTendersData, null); + expect(viewModel.isLoadingMore, false); + expect(viewModel.hasMore, true); + expect(viewModel.isLoadingMoreTenders, false); + expect(viewModel.hasMoreTenders, true); + expect(viewModel.selectedDateRange, null); + expect(viewModel.selectedStatus, 'All'); + expect(viewModel.currentPage, 1); + }); + + test('should format dates correctly when no date range is selected', () { + expect(viewModel.startDateFormatted, 'YYYY/MM/DD'); + expect(viewModel.endDateFormatted, 'YYYY/MM/DD'); + expect(viewModel.startDateUnix, ''); + expect(viewModel.endDateUnix, ''); + }); + + test('should calculate total pages correctly when no data', () { + expect(viewModel.totalPages, 1); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_pagination_test.dart b/test/view_models/your_tenders_view_model_pagination_test.dart new file mode 100644 index 0000000..34d6ccf --- /dev/null +++ b/test/view_models/your_tenders_view_model_pagination_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../fixtures/test_fixtures.dart'; +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel Pagination', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + test('should calculate total pages from tender data', () { + viewModel.data = TestFixtures.tenderApprovalsResponse; + expect(viewModel.totalPages, 1); + }); + + test('should calculate total pages from feedback data', () { + viewModel.likedTendersData = TestFixtures.likedTendersResponse; + expect(viewModel.totalPages, 1); + }); + + test('should return 1 when no data available', () { + expect(viewModel.totalPages, 1); + }); + + test('should jump to page successfully', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'All'; + await viewModel.jumpToPage(2); + + expect(viewModel.currentPage, 2); + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + expect(viewModel.likedTendersData, null); + expect(viewModel.hasMore, true); + expect(viewModel.hasMoreTenders, true); + }); + + test('should load more tenders for all status', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'All'; + await viewModel.loadMoreTenders(); + + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + }); + + test('should load more tenders for submitted status', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'Tenders Submitted'; + await viewModel.loadMoreTenders(); + + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + }); + + test('should not load more tenders when already loading', () async { + viewModel.isLoadingMoreTenders = true; + await viewModel.loadMoreTenders(); + + expect(viewModel.data, null); + }); + + test('should not load more tenders when no more data', () async { + viewModel.hasMoreTenders = false; + await viewModel.loadMoreTenders(); + + expect(viewModel.data, null); + }); + + test('should handle pagination with date range', () async { + mockRepository.setGetTendersResult( + const Result.ok(TestFixtures.tenderApprovalsResponse), + ); + + viewModel.selectedStatus = 'All'; + viewModel.setDateRange( + DateTimeRange(start: DateTime(2024, 1, 1), end: DateTime(2024, 1, 31)), + ); + + await viewModel.loadMoreTenders(); + + expect(viewModel.data, TestFixtures.tenderApprovalsResponse); + }); + + test('should reset pagination state when clearing data', () { + // Set some pagination state + viewModel.currentPage = 3; + viewModel.data = TestFixtures.tenderApprovalsResponse; + viewModel.likedTendersData = TestFixtures.likedTendersResponse; + + viewModel.clearFilter(); + + expect(viewModel.currentPage, 1); + expect(viewModel.data, null); + expect(viewModel.likedTendersData, null); + }); + }); +} diff --git a/test/view_models/your_tenders_view_model_utility_test.dart b/test/view_models/your_tenders_view_model_utility_test.dart new file mode 100644 index 0000000..daba893 --- /dev/null +++ b/test/view_models/your_tenders_view_model_utility_test.dart @@ -0,0 +1,199 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/constants/assets.dart'; +import 'package:tm_app/core/constants/tender_approval_status.dart'; +import 'package:tm_app/core/constants/tender_feedback.dart'; +import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; + +import '../mocks/mock_your_tenders_repository.dart'; + +void main() { + group('YourTendersViewModel Utility Methods', () { + late MockYourTendersRepository mockRepository; + late YourTendersViewModel viewModel; + + setUp(() { + mockRepository = MockYourTendersRepository(); + viewModel = YourTendersViewModel(yourTendersRepository: mockRepository); + }); + + tearDown(() { + viewModel.dispose(); + }); + + group('getBackGroundColorByStatus', () { + test('should return green for liked status', () { + final color = viewModel.getBackGroundColorByStatus( + TenderFeedback.liked.value, + ); + expect(color, AppColors.green0); + }); + + test('should return green for submitted status', () { + final color = viewModel.getBackGroundColorByStatus( + TenderApprovalStatus.submitted.value, + ); + expect(color, AppColors.green0); + }); + + test('should return red for disliked status', () { + final color = viewModel.getBackGroundColorByStatus( + TenderFeedback.disliked.value, + ); + expect(color, AppColors.red0); + }); + + test('should return red for rejected status', () { + final color = viewModel.getBackGroundColorByStatus( + TenderApprovalStatus.rejected.value, + ); + expect(color, AppColors.red0); + }); + + test('should return grey for unknown status', () { + final color = viewModel.getBackGroundColorByStatus('unknown'); + expect(color, AppColors.grey0); + }); + }); + + group('getBorderColorByStatus', () { + test('should return green for liked status', () { + final color = viewModel.getBorderColorByStatus( + TenderFeedback.liked.value, + ); + expect(color, AppColors.green10); + }); + + test('should return green for submitted status', () { + final color = viewModel.getBorderColorByStatus( + TenderApprovalStatus.submitted.value, + ); + expect(color, AppColors.green10); + }); + + test('should return red for disliked status', () { + final color = viewModel.getBorderColorByStatus( + TenderFeedback.disliked.value, + ); + expect(color, AppColors.red10); + }); + + test('should return red for rejected status', () { + final color = viewModel.getBorderColorByStatus( + TenderApprovalStatus.rejected.value, + ); + expect(color, AppColors.red10); + }); + + test('should return grey for unknown status', () { + final color = viewModel.getBorderColorByStatus('unknown'); + expect(color, AppColors.grey30); + }); + }); + + group('getStatusCardColorByStatus', () { + test('should return green for liked status', () { + final color = viewModel.getStatusCardColorByStatus( + TenderFeedback.liked.value, + ); + expect(color, AppColors.green20); + }); + + test('should return green for submitted status', () { + final color = viewModel.getStatusCardColorByStatus( + TenderApprovalStatus.submitted.value, + ); + expect(color, AppColors.green20); + }); + + test('should return red for disliked status', () { + final color = viewModel.getStatusCardColorByStatus( + TenderFeedback.disliked.value, + ); + expect(color, AppColors.red10); + }); + + test('should return red for rejected status', () { + final color = viewModel.getStatusCardColorByStatus( + TenderApprovalStatus.rejected.value, + ); + expect(color, AppColors.red10); + }); + + test('should return grey for unknown status', () { + final color = viewModel.getStatusCardColorByStatus('unknown'); + expect(color, AppColors.grey0); + }); + }); + + group('getStatusTextColorByStatus', () { + test('should return green for liked status', () { + final color = viewModel.getStatusTextColorByStatus( + TenderFeedback.liked.value, + ); + expect(color, AppColors.green30); + }); + + test('should return green for submitted status', () { + final color = viewModel.getStatusTextColorByStatus( + TenderApprovalStatus.submitted.value, + ); + expect(color, AppColors.green30); + }); + + test('should return red for disliked status', () { + final color = viewModel.getStatusTextColorByStatus( + TenderFeedback.disliked.value, + ); + expect(color, AppColors.red20); + }); + + test('should return red for rejected status', () { + final color = viewModel.getStatusTextColorByStatus( + TenderApprovalStatus.rejected.value, + ); + expect(color, AppColors.red20); + }); + + test('should return grey for unknown status', () { + final color = viewModel.getStatusTextColorByStatus('unknown'); + expect(color, AppColors.grey60); + }); + }); + + group('getStatusIconByStatus', () { + test('should return tick circle for liked status', () { + final icon = viewModel.getStatusIconByStatus( + TenderFeedback.liked.value, + ); + expect(icon, AssetsManager.tickCircle); + }); + + test('should return tick circle for submitted status', () { + final icon = viewModel.getStatusIconByStatus( + TenderApprovalStatus.submitted.value, + ); + expect(icon, AssetsManager.tickCircle); + }); + + test('should return close circle for disliked status', () { + final icon = viewModel.getStatusIconByStatus( + TenderFeedback.disliked.value, + ); + expect(icon, AssetsManager.closeCircle); + }); + + test('should return close circle for rejected status', () { + final icon = viewModel.getStatusIconByStatus( + TenderApprovalStatus.rejected.value, + ); + expect(icon, AssetsManager.closeCircle); + }); + + test('should return null for unknown status', () { + final icon = viewModel.getStatusIconByStatus('unknown'); + expect(icon, null); + }); + }); + }); +}