From 7d74da929f129d49d786cb6e49091a027750dd92 Mon Sep 17 00:00:00 2001 From: Parag Gupta <103507835+Dante291@users.noreply.github.com> Date: Wed, 20 Dec 2023 00:08:12 +0530 Subject: [PATCH 01/29] Test for join_organisation_after_auth.dart (#2260) * Test for join_organisation_after_auth.dart * modify * adding more tests for complete coverage --- .../join_organisation_after_auth_test.dart | 115 ++++++++++++++++-- 1 file changed, 103 insertions(+), 12 deletions(-) diff --git a/test/views/after_auth_screens/join_organisation_after_auth_test.dart b/test/views/after_auth_screens/join_organisation_after_auth_test.dart index 53d67e89d4..1fdcef83b8 100644 --- a/test/views/after_auth_screens/join_organisation_after_auth_test.dart +++ b/test/views/after_auth_screens/join_organisation_after_auth_test.dart @@ -4,26 +4,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:talawa/enums/enums.dart'; import 'package:talawa/models/organization/org_info.dart'; import 'package:talawa/models/user/user_info.dart'; import 'package:talawa/services/graphql_config.dart'; import 'package:talawa/services/size_config.dart'; import 'package:talawa/utils/app_localization.dart'; -import 'package:talawa/view_model/lang_view_model.dart'; import 'package:talawa/view_model/pre_auth_view_models/select_organization_view_model.dart'; import 'package:talawa/views/after_auth_screens/join_org_after_auth/join_organisation_after_auth.dart'; import 'package:talawa/views/base_view.dart'; import 'package:talawa/widgets/organization_search_list.dart'; import '../../helpers/test_helpers.dart'; +import '../../helpers/test_helpers.mocks.dart'; import '../../helpers/test_locator.dart'; -Widget createJoinOrgAfterAuth({String orgId = "fake_id"}) { - return BaseView( - onModelReady: (model) => model.initialize(), - builder: (context, langModel, child) { +Widget createJoinOrgAfterAuth({ + String orgId = "fake_id", +}) { + return BaseView( + onModelReady: (model) => model.initialise(orgId), + builder: (context, model, child) { return MaterialApp( + navigatorKey: navigationService.navigatorKey, locale: const Locale('en'), localizationsDelegates: const [ AppLocalizationsDelegate(isTest: true), @@ -53,6 +58,99 @@ void main() { }); group("Tests for JoinOrganizationAfterAuth - widgets", () { + testWidgets('QR Scan Test', (WidgetTester tester) async { + final controller = MockQRViewController(); + when(controller.scannedDataStream).thenAnswer((_) async* { + yield Barcode( + ' ' + '?orgid=6737904485008f171cf29924', + BarcodeFormat.qrcode, + null, + ); + }); + when(controller.stopCamera()) + .thenAnswer((realInvocation) => Future.value()); + + await tester.pumpWidget( + createJoinOrgAfterAuth(), + ); + + await tester.pumpAndSettle(const Duration(seconds: 6)); + + await tester.tap(find.byIcon(Icons.qr_code_scanner)); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is ClipRRect && + widget.child is Container && + (widget.child! as Container).child is Column, + ), + findsOneWidget, + ); + (tester.widget(find.byType(QRView)) as QRView) + .onQRViewCreated(controller); + }); + testWidgets('QR Scan Test when url != GraphqlConfig.orgURI', + (WidgetTester tester) async { + final controller = MockQRViewController(); + when(controller.scannedDataStream).thenAnswer((_) async* { + yield Barcode( + '1' + '?orgid=6737904485008f171cf29924', + BarcodeFormat.qrcode, + null, + ); + }); + when(controller.stopCamera()) + .thenAnswer((realInvocation) => Future.value()); + + await tester.pumpWidget( + createJoinOrgAfterAuth(), + ); + + await tester.pumpAndSettle(const Duration(seconds: 6)); + + await tester.tap(find.byIcon(Icons.qr_code_scanner)); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is ClipRRect && + widget.child is Container && + (widget.child! as Container).child is Column, + ), + findsOneWidget, + ); + (tester.widget(find.byType(QRView)) as QRView) + .onQRViewCreated(controller); + }); + testWidgets('Test _onQRViewCreated when throwing exception', + (WidgetTester tester) async { + final controller = MockQRViewController(); + when(controller.scannedDataStream).thenAnswer((_) async* { + yield Barcode( + ' ' + '?orgid=6737904485008f171cf29924', + BarcodeFormat.qrcode, + null, + ); + }); + when(controller.stopCamera()) + .thenAnswer((realInvocation) => Future.value()); + + await tester.pumpWidget( + createJoinOrgAfterAuth(), + ); + when(controller.stopCamera()).thenThrow(Exception("exception")); + + await tester.pumpAndSettle(const Duration(seconds: 6)); + + await tester.tap(find.byIcon(Icons.qr_code_scanner)); + await tester.pumpAndSettle(); + + (tester.widget(find.byType(QRView)) as QRView) + .onQRViewCreated(controller); + }); testWidgets( "Check if JoinOrganizationsAfterAuth shows up", (tester) async { @@ -132,13 +230,6 @@ void main() { /// Search is No-Longer is a feature, if it gets implemented in future use this test /// Really good test to learn from so not deleting testWidgets("Check if model related functions work", (tester) async { - // Registers a singleton, which means that every instance of - // SelectOrganizationViewModel will be the same. - locator.unregister(); - locator.registerSingleton( - SelectOrganizationViewModel(), - ); - final orgOne = OrgInfo( name: "org_one", creatorInfo: User( From a12294f8cb573946ac0e9be6d65f89e6ad2e870e Mon Sep 17 00:00:00 2001 From: Nidhin V Ninan <131900819+nidhin29@users.noreply.github.com> Date: Wed, 20 Dec 2023 23:53:54 +0530 Subject: [PATCH 02/29] Unittest for custom_view_modal.dart (#2250) * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified * modified --- .../custom_drawer_view_model.dart | 3 +- .../custom_drawer_view_model_test.dart | 252 ++++++++++++++---- 2 files changed, 204 insertions(+), 51 deletions(-) diff --git a/lib/view_model/widgets_view_models/custom_drawer_view_model.dart b/lib/view_model/widgets_view_models/custom_drawer_view_model.dart index 0abcb07420..1daab37b25 100644 --- a/lib/view_model/widgets_view_models/custom_drawer_view_model.dart +++ b/lib/view_model/widgets_view_models/custom_drawer_view_model.dart @@ -75,7 +75,8 @@ class CustomDrawerViewModel extends BaseModel { /// None void switchOrg(OrgInfo switchToOrg) { // if `selectedOrg` is equal to `switchOrg` and `switchToOrg` present or not. - if (selectedOrg == switchToOrg && isPresentinSwitchableOrg(switchToOrg)) { + if ((selectedOrg == switchToOrg) && + (isPresentinSwitchableOrg(switchToOrg))) { // _navigationService.pop(); navigationService.showTalawaErrorSnackBar( '${switchToOrg.name} already selected', diff --git a/test/view_model_tests/custom_drawer_view_model_test.dart b/test/view_model_tests/custom_drawer_view_model_test.dart index cb4b695f7e..ee24e31fff 100644 --- a/test/view_model_tests/custom_drawer_view_model_test.dart +++ b/test/view_model_tests/custom_drawer_view_model_test.dart @@ -1,25 +1,38 @@ -// ignore_for_file: talawa_api_doc -// ignore_for_file: talawa_good_doc_comments - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:talawa/enums/enums.dart'; import 'package:talawa/models/organization/org_info.dart'; import 'package:talawa/models/user/user_info.dart'; import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/size_config.dart'; import 'package:talawa/view_model/main_screen_view_model.dart'; import 'package:talawa/view_model/widgets_view_models/custom_drawer_view_model.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + import '../helpers/test_helpers.dart'; import '../helpers/test_locator.dart'; +/// Mocked context. +/// +/// more_info_if_required class MockBuildContext extends Mock implements BuildContext {} +/// Main. +/// +/// more_info_if_required +/// +/// **params**: +/// None +/// +/// **returns**: +/// None void main() { - int testCount = 0; testSetupLocator(); locator().test(); locator().test(); + locator(); setUp(() { registerServices(); @@ -29,21 +42,103 @@ void main() { tearDown(() { unregisterServices(); }); + group('CustomDrawerViewModel Tests', () { + test('switchAbleOrg should correctly get and set value', () { + final model = CustomDrawerViewModel(); + final orgList = [ + OrgInfo(name: 'Test Org 1'), + OrgInfo(name: 'Test Org 2'), + ]; + + model.switchAbleOrg = orgList; + + expect(model.switchAbleOrg, equals(orgList)); + }); + test("initialize should setup the model with user's joined organizations", + () { + final homeModel = MainScreenViewModel(); + final MockBuildContext mockContext = MockBuildContext(); + final model = CustomDrawerViewModel(); + final user = User(joinedOrganizations: [OrgInfo(name: 'Test Org')]); + + when(userConfig.currentOrgInfoStream) + .thenAnswer((_) => Stream.value(OrgInfo())); + when(userConfig.currentUser).thenReturn(user); + when(userConfig.currentOrg).thenReturn(OrgInfo()); + + model.initialize(homeModel, mockContext); + + expect(model.switchAbleOrg, equals(user.joinedOrganizations)); + }); + + test('switchOrg should show info message if different organization', () { + final model = CustomDrawerViewModel(); + final orgInfo = OrgInfo(name: 'Test Org'); + + when(userConfig.currentOrg).thenReturn(OrgInfo(name: 'Current Org')); + model.switchAbleOrg = [orgInfo]; + + model.switchOrg(orgInfo); + + verify( + navigationService.showTalawaErrorSnackBar( + 'Switched to ${orgInfo.name}', + MessageType.info, + ), + ); + }); + + test('switchOrg should pop navigation after switching or showing error', + () { + final model = CustomDrawerViewModel(); + final orgInfo = OrgInfo(name: 'Test Org'); + + when(userConfig.currentOrg).thenReturn(OrgInfo(name: 'Current Org')); + model.switchAbleOrg = [orgInfo]; + + model.switchOrg(orgInfo); + + verify(navigationService.pop()); + }); + + test('initialize should setup the model with userConfig values', () { + final homeModel = MainScreenViewModel(); + final MockBuildContext mockContext = MockBuildContext(); + final model = CustomDrawerViewModel(); + final user = User(joinedOrganizations: [OrgInfo(name: 'Test Org')]); + + when(userConfig.currentOrgInfoStream) + .thenAnswer((_) => Stream.value(OrgInfo())); + when(userConfig.currentUser).thenReturn(user); + when(userConfig.currentOrg).thenReturn(OrgInfo()); + + model.initialize(homeModel, mockContext); + + expect(model.switchAbleOrg, equals(user.joinedOrganizations)); + expect(model.selectedOrg, equals(userConfig.currentOrg)); + }); + test( + 'switchOrg should save new organization in userConfig if different organization', + () { + final model = CustomDrawerViewModel(); + final orgInfo = OrgInfo(name: 'Test Org'); - group('Custom Drawer Model testing -', () { - //final mockConnectivity = getAndRegisterConnectivityService(); - final mainscreenModel = MainScreenViewModel(); - final model = CustomDrawerViewModel(); - final MockBuildContext mockBuildContext = MockBuildContext(); - //final UserConfig mockus - tearDown(() { - if (testCount == 5) { - model.dispose(); - } + when(userConfig.currentOrg).thenReturn(OrgInfo(name: 'Current Org')); + model.switchAbleOrg = [orgInfo]; + + model.switchOrg(orgInfo); + + verify(userConfig.saveCurrentOrgInHive(orgInfo)); + verify( + navigationService.showTalawaErrorSnackBar( + 'Switched to ${orgInfo.name}', + MessageType.info, + ), + ); }); test('check if switchOrg is working with zero switchable orgs', () { - print("1"); + final model = CustomDrawerViewModel(); model.setSelectedOrganizationName(userConfig.currentOrg); //No switchable org are present in the model @@ -55,12 +150,11 @@ void main() { //check if selected org is mocked joined org .Expectation-false. expect(model.selectedOrg, isNot(mockJoinedOrg)); - testCount++; }); test('check if switchOrg is working with wrong switchable org being passed', () { - print("2"); + final model = CustomDrawerViewModel(); model.setSelectedOrganizationName(userConfig.currentOrg); //Mock switchable org are present in the model @@ -83,13 +177,14 @@ void main() { expect(isPresent, false); //check if selected org is changed or not. Expected-Not changing expect(model.selectedOrg, isNot(fakeOrg)); - testCount++; }); test('check if switchOrg is working with mock joined orgs', () async { - print("3"); + final model = CustomDrawerViewModel(); + final homeModel = MainScreenViewModel(); + final MockBuildContext mockContext = MockBuildContext(); //Intializing a mock model with mockBuildContext - model.initialize(mainscreenModel, mockBuildContext); + model.initialize(homeModel, mockContext); //Storing the first switchable org in mockOrgInfo final OrgInfo mockChangeOrgTo = model.switchAbleOrg.first; @@ -98,35 +193,92 @@ void main() { //expecting the selected org will be equal to the mockChangeOrgto returns true expect(model.selectedOrg, mockChangeOrgTo); - testCount++; - }); - - // test('check if switchOrg is working with already joined mock orgs', - // () async { - // print("4"); - // //Intializing a mock model with mockBuildContext - // // model.initialize(mainscreenModel, mockBuildContext); - // //Storing the first switchable org in mockOrgInfo - // final OrgInfo mockChangeOrgTo = model.switchAbleOrg.first; - // //Calling the switchOrg function - // model.switchOrg(mockChangeOrgTo); - // model.switchOrg(mockChangeOrgTo); - - // //expecting the selected org will be equal to the mockChangeOrgto returns true - // expect(model.selectedOrg, mockChangeOrgTo); - // testCount++; - // }); - - // test('check if switchOrg is working with switching joined mock orgs', - // () async { - // print("5"); - // // model.initialize(mainscreenModel, mockBuildContext); - // final OrgInfo mockChangeOrgTo = model.switchAbleOrg.first; - // final OrgInfo mockChangeOrgToLast = model.switchAbleOrg.last; - // model.switchOrg(mockChangeOrgTo); - // model.switchOrg(mockChangeOrgToLast); - // expect(model.selectedOrg, mockChangeOrgToLast); - // testCount++; - // }); + }); + + test('setSelectedOrganizationName should update selectedOrg if different', + () { + final model = CustomDrawerViewModel(); + final orgInfo = OrgInfo(name: 'Test Org'); + + model.setSelectedOrganizationName(orgInfo); + + expect(model.selectedOrg, equals(orgInfo)); + }); + + test('Check if OrgInfo is present in switchAbleOrg', () { + final model = CustomDrawerViewModel(); + model.switchAbleOrg = [ + OrgInfo(id: '1'), + OrgInfo(id: '2'), + OrgInfo(id: '3'), + ]; + final switchToOrg = OrgInfo(id: '2'); + + final result = model.isPresentinSwitchableOrg(switchToOrg); + + expect(result, true); + }); + + test('Check if OrgInfo is not present in switchAbleOrg', () { + final model = CustomDrawerViewModel(); + model.switchAbleOrg = [ + OrgInfo(id: '1'), + OrgInfo(id: '2'), + OrgInfo(id: '3'), + ]; + final switchToOrg = OrgInfo(id: '4'); + + final result = model.isPresentinSwitchableOrg(switchToOrg); + + expect(result, false); + }); + + test( + 'setSelectedOrganizationName should show error snackbar if org is same as selected', + () { + final homeModel = MainScreenViewModel(); + final MockBuildContext mockContext = MockBuildContext(); + final model = CustomDrawerViewModel(); + final user = + User(joinedOrganizations: [OrgInfo(id: '1', name: 'Test Org1')]); + + when(userConfig.currentOrgInfoStream) + .thenAnswer((_) => Stream.value(OrgInfo(id: '1', name: 'Test Org1'))); + when(userConfig.currentUser).thenReturn(user); + when(userConfig.currentOrg) + .thenReturn(OrgInfo(id: '1', name: 'Test Org1')); + model.initialize(homeModel, mockContext); + final switchToOrg = OrgInfo(id: '1', name: 'Test Org1'); + model.setSelectedOrganizationName(switchToOrg); + final result1 = model.isPresentinSwitchableOrg(switchToOrg); + + expect(result1, true); + // expect(model.selectedOrg, equals(userConfig.currentOrg)); + model.switchOrg(switchToOrg); + final result = model.isPresentinSwitchableOrg(switchToOrg); + + expect(result, true); + expect(model.selectedOrg, equals(switchToOrg)); + verify( + navigationService.showTalawaErrorSnackBar( + '${switchToOrg.name} already selected', + MessageType.warning, + ), + ).called(1); + }); + test('controller should return ScrollController instance', () { + final model = CustomDrawerViewModel(); + expect(model.controller, isA()); + }); + + test('targets should return List instance', () { + final model = CustomDrawerViewModel(); + expect(model.targets, isA>()); + }); + + test('selectedOrg should be initially null', () { + final model = CustomDrawerViewModel(); + expect(model.selectedOrg, isNull); + }); }); } From 3ecd3fc3389a692ba1da01635db2b8fd2500264c Mon Sep 17 00:00:00 2001 From: Shaik Azad <120930148+Azad99-9@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:45:03 +0530 Subject: [PATCH 03/29] Test user config (#2258) * resolved conflicts * fetchmore result typecast * user_config.dart made 100% code coverage. --- lib/services/user_config.dart | 2 + test/helpers/test_helpers.dart | 2 + test/service_tests/user_config_test.dart | 113 +++++++++++++++++++++-- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/lib/services/user_config.dart b/lib/services/user_config.dart index 73fcfa739d..62683ad023 100644 --- a/lib/services/user_config.dart +++ b/lib/services/user_config.dart @@ -84,6 +84,7 @@ class UserConfig { _currentOrgInfoController.add(_currentOrg!); _currentUser = boxUser.get('user'); + // if there is not currentUser then returns false. if (_currentUser == null) { _currentUser = User(id: 'null', authToken: 'null'); @@ -108,6 +109,7 @@ class UserConfig { _currentOrgInfoController.add(_currentOrg!); saveUserInHive(); + return true; } on Exception catch (e) { print(e); diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 6bafa6645d..4fca029420 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -243,6 +243,8 @@ GraphqlConfig getAndRegisterGraphqlConfig() { ); }); + when(service.getToken()).thenAnswer((_) async => "sample_token"); + locator.registerSingleton(service); return service; } diff --git a/test/service_tests/user_config_test.dart b/test/service_tests/user_config_test.dart index 8f842156de..ef0bd9d787 100644 --- a/test/service_tests/user_config_test.dart +++ b/test/service_tests/user_config_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: talawa_api_doc // ignore_for_file: talawa_good_doc_comments +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -78,7 +79,81 @@ void main() async { setUpAll(() { registerServices(); }); - test('Test for User log out.', () async { + + test('Test for getters & setters.', () { + final model = UserConfig(); + + // model.currentOrgInfoController + expect(model.currentOrgInfoController, isA>()); + + // model.currentOrgName + expect(model.currentOrgName, isA()); + + // model.currenOrg (setter) + model.currentOrg = OrgInfo(name: 'org'); + + // print(model.currentOrgInfoController); + }); + + test('Test for userLoggedIn method.', () async { + final model = UserConfig(); + model.currentUser.id = 'fake_id'; + + userBox.put('user', User(id: 'fake', firstName: 'first')); + + final Map data = { + 'users': [ + { + '_id': '1234567890', + 'firstName': 'John', + 'lastName': 'Doe', + 'email': 'johndoe@example.com', + 'image': 'https://example.com/profile.jpg', + 'accessToken': 'exampleAccessToken', + 'refreshToken': 'exampleRefreshToken', + } + ], + }; + + when( + databaseFunctions.gqlAuthQuery( + queries.fetchUserInfo, + variables: anyNamed('variables'), + ), + ).thenAnswer((_) async { + return QueryResult( + source: QueryResultSource.network, + data: data, + options: QueryOptions(document: gql(queries.fetchUserInfo)), + ); + }); + + // if there is _currentUser. + bool loggedIn = await model.userLoggedIn(); + expect(loggedIn, true); + + userBox.delete('user'); + + // if there is no _currentUser. + loggedIn = await model.userLoggedIn(); + expect(loggedIn, false); + + when( + databaseFunctions.gqlAuthQuery( + queries.fetchUserInfo, + variables: anyNamed('variables'), + ), + ).thenAnswer((_) async { + throw Exception('Simulated Exception.'); + }); + + // show couldn't update errorsnackbar. + loggedIn = await model.userLoggedIn(); + expect(loggedIn, true); + // print(model.currentUser); + }); + + test('Test for User log out method.', () async { databaseFunctions.init(); when(databaseFunctions.gqlAuthMutation(queries.logout())) @@ -120,7 +195,7 @@ void main() async { expect(loggedOut, false); }); - test('Test for updateUserJoinedOrg', () async { + test('Test for updateUserJoinedOrg method', () async { final model = UserConfig(); model.currentUser = mockUser; @@ -129,7 +204,7 @@ void main() async { expect(mockUser.joinedOrganizations, mockOrgDetails); }); - test('Test for updateUserCreatedOrg', () async { + test('Test for updateUserCreatedOrg method', () async { final model = UserConfig(); model.currentUser = mockUser; @@ -138,7 +213,7 @@ void main() async { expect(mockUser.createdOrganizations, mockOrgDetails); }); - test('Test for updateUserMemberRequestOrg', () async { + test('Test for updateUserMemberRequestOrg method', () async { final model = UserConfig(); model.currentUser = mockUser; final expected = [...mockUser.membershipRequests!, ...mockOrgDetails]; @@ -147,7 +222,7 @@ void main() async { expect(mockUser.membershipRequests, expected); }); - test('Test for updateUserAdminOrg', () async { + test('Test for updateUserAdminOrg method', () async { final model = UserConfig(); model.currentUser = mockUser; @@ -156,7 +231,7 @@ void main() async { expect(mockUser.adminFor, mockOrgDetails); }); - test('Test for updateAccessToken', () async { + test('Test for updateAccessToken method.', () async { final model = UserConfig(); model.currentUser = mockUser; const newAuthToken = 'newAccessToken'; @@ -170,5 +245,31 @@ void main() async { expect(mockUser.authToken, newAuthToken); expect(mockUser.refreshToken, newRefreshToken); }); + + test('Test for saveCurrentOrgInHive method.', () async { + final model = UserConfig(); + model.currentUser = mockUser; + + // To test the box.get('org') != null condition. + orgBox.put('org', OrgInfo(id: 'fakeId', name: 'org')); + model.saveCurrentOrgInHive(mockOrgDetails[0]); + + // To test the box.get('org') == null condition. + orgBox.delete('org'); + model.saveCurrentOrgInHive(mockOrgDetails[0]); + }); + + test('Test for updateUser method.', () async { + final model = UserConfig(); + + when(databaseFunctions.init()).thenAnswer((_) { + throw Exception('simulated exception.'); + }); + + final updated = await model.updateUser(User(id: 'sampleId')); + + // user updation failed. + expect(!updated, true); + }); }); } From 1401b74178a2d2abdcd4fd171bbd57517503802c Mon Sep 17 00:00:00 2001 From: Vaidic Dodwani <59657947+vaidic-dodwani@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:50:51 +0530 Subject: [PATCH 04/29] =?UTF-8?q?Edit:=20Made=20changes=20to=20follow=20th?= =?UTF-8?q?e=20custom=20lints=20patter=20and=20updates=20the=20=E2=80=A6?= =?UTF-8?q?=20(#2118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Edit: Made changes to follow the custom lints patter and updates the package * Removed unused function in the edit_profile_view_model * added tests for convertToBase64 function --- .../edit_profile_view_model.dart | 21 ---------------- pubspec.lock | 24 +++++++++---------- .../widgets/lang_switch_test.dart | 2 +- 3 files changed, 13 insertions(+), 34 deletions(-) diff --git a/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart b/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart index be1a52fdb3..b65a258320 100644 --- a/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart +++ b/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart @@ -1,6 +1,4 @@ -import 'dart:convert'; import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:talawa/locator.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; @@ -63,25 +61,6 @@ class EditProfilePageViewModel extends BaseModel { } } - /// This function is used to convert the image into Base64 format. - /// - /// **params**: - /// * `file`: Takes the image in format of file. - /// - /// **returns**: - /// * `Future`: image in string format - Future convertToBase64(File file) async { - try { - final List bytes = await file.readAsBytes(); - final String base64String = base64Encode(bytes); - print(base64String); - imageFile = base64String as File?; - return base64String; - } catch (error) { - return ''; - } - } - /// This function remove the selected image. /// /// **params**: diff --git a/pubspec.lock b/pubspec.lock index 8cebab539a..b878a957d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" connectivity_plus: dependency: "direct main" description: @@ -1049,10 +1049,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: @@ -1518,18 +1518,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: @@ -1597,10 +1597,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timelines: dependency: "direct main" description: @@ -1781,10 +1781,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: diff --git a/test/widget_tests/widgets/lang_switch_test.dart b/test/widget_tests/widgets/lang_switch_test.dart index 9c920cbbdc..02b6be38fe 100644 --- a/test/widget_tests/widgets/lang_switch_test.dart +++ b/test/widget_tests/widgets/lang_switch_test.dart @@ -16,7 +16,7 @@ import '../../helpers/test_locator.dart'; Widget createLanguageTile() { return BaseView( - onModelReady: (model) => model.initialize(), + onModelReady: (appLanguageModel) => appLanguageModel.initialize(), builder: (_, __, ___) => MaterialApp( localizationsDelegates: [ const AppLocalizationsDelegate(isTest: true), From f292741731794dbc14ab858cd2caaa90076c85a4 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Sun, 24 Dec 2023 01:13:28 +0530 Subject: [PATCH 05/29] News feed comment fix (#2248) * Updated the code * Updated * Updated * Updated * Updated * Updated * Updated * Updated * Fixed View Model Test * Updated * Updated * Updated * Update pubspec.lock * fixed order * Updated --- lib/services/comment_service.dart | 47 ++-- lib/utils/post_queries.dart | 46 ++++ lib/view_model/access_request_view_model.dart | 2 - .../comments_view_model.dart | 66 +++-- test/helpers/test_helpers.mocks.dart | 27 +- test/service_tests/comment_service_test.dart | 249 ++++++++++++++---- 6 files changed, 342 insertions(+), 95 deletions(-) diff --git a/lib/services/comment_service.dart b/lib/services/comment_service.dart index 655a2b476e..dc457e5048 100644 --- a/lib/services/comment_service.dart +++ b/lib/services/comment_service.dart @@ -1,9 +1,8 @@ -// ignore_for_file: talawa_api_doc, avoid_dynamic_calls -// ignore_for_file: talawa_good_doc_comments - +import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:talawa/locator.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/utils/comment_queries.dart'; +import 'package:talawa/utils/post_queries.dart'; /// CommentService class have different member functions which provides service in the context of commenting. /// @@ -18,11 +17,14 @@ class CommentService { /// This function is used to add comment on the post. /// - /// parameters: - /// * [postId] - Post id where comment need to be added. - /// * [text] - content of the comment. + /// To verify things are working, check out the native platform logs. + /// **params**: + /// * `postId`: The post id on which comment is to be added. + /// * `text`: The comment text. + /// + /// **returns**: + /// * `Future`: promise that will be fulfilled message background activities are successful. Future createComments(String postId, String text) async { - print("comment service called"); final String createCommentQuery = CommentQueries().createComment(); final result = await _dbFunctions.gqlAuthMutation( createCommentQuery, @@ -31,21 +33,30 @@ class CommentService { 'text': text, }, ); - print("comment added"); - print(result); return result; } - /// This function is used to fetch all comments on the post. + /// This function is used to get all comments on the post. + /// + /// To verify things are working, check out the native platform logs. + /// **params**: + /// * `postId`: The post id for which comments are to be fetched. + /// + /// **returns**: + /// * `Future>`: promise that will be fulfilled with list of comments. /// - /// parameters: - /// * [postId] - Post id for which comments need to be fetched. - Future getCommentsForPost(String postId) async { - final String getCommmentQuery = CommentQueries().getPostsComments(postId); - final result = await _dbFunctions.gqlAuthMutation(getCommmentQuery); - if (result.data != null) { - return result.data["commentsByPost"] as List; + Future> getCommentsForPost(String postId) async { + final String getCommmentQuery = PostQueries().getPostById(postId); + + final dynamic result = await _dbFunctions.gqlAuthMutation(getCommmentQuery); + + if (result == null) { + return []; } - return []; + final resultData = (result as QueryResult).data; + + final resultDataPostComments = (resultData?['post'] + as Map)['comments'] as List; + return resultDataPostComments; } } diff --git a/lib/utils/post_queries.dart b/lib/utils/post_queries.dart index c5b73d922f..40cff4d0d6 100644 --- a/lib/utils/post_queries.dart +++ b/lib/utils/post_queries.dart @@ -40,6 +40,52 @@ class PostQueries { """; } + /// Getting Post by Post Id. + /// + /// **params**: + /// * `postId`: The post id + /// + /// **returns**: + /// * `String`: The query related to gettingPostsbyId + String getPostById(String postId) { + return """ + query { + post(id: "$postId") + { + _id + text + createdAt + imageUrl + videoUrl + title + commentCount + likeCount + creator{ + _id + firstName + lastName + image + } + organization{ + _id + } + likedBy{ + _id + } + comments{ + _id, + text, + createdAt + creator{ + firstName + lastName + } + } + } + } +"""; + } + /// Add Like to a post. /// /// **params**: diff --git a/lib/view_model/access_request_view_model.dart b/lib/view_model/access_request_view_model.dart index a2e8648ab9..1d539c065d 100644 --- a/lib/view_model/access_request_view_model.dart +++ b/lib/view_model/access_request_view_model.dart @@ -1,4 +1,3 @@ -// ignore_for_file: talawa_api_doc import 'package:flutter/cupertino.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:talawa/constants/routing_constants.dart'; @@ -23,7 +22,6 @@ class AccessScreenViewModel extends BaseModel { /// initialization function. /// - /// /// **params**: /// * `org`: Org to send request to. /// diff --git a/lib/view_model/widgets_view_models/comments_view_model.dart b/lib/view_model/widgets_view_models/comments_view_model.dart index f189e528ae..4bc9b31ef0 100644 --- a/lib/view_model/widgets_view_models/comments_view_model.dart +++ b/lib/view_model/widgets_view_models/comments_view_model.dart @@ -1,6 +1,3 @@ -// ignore_for_file: talawa_api_doc -// ignore_for_file: talawa_good_doc_comments - import 'package:talawa/enums/enums.dart'; import 'package:talawa/locator.dart'; import 'package:talawa/models/comment/comment_model.dart'; @@ -9,25 +6,41 @@ import 'package:talawa/services/post_service.dart'; import 'package:talawa/services/user_config.dart'; import 'package:talawa/view_model/base_view_model.dart'; -/// CommentsViewModel class helps to serve the data from model -/// and to react to user's input for Comment Widget. +/// CommentsViewModel class helps to serve the data from model and to react to user's input for Comment Widget. /// /// Methods include: /// * `getComments` : to get all comments on the post. /// * `createComment` : to add comment on the post. class CommentsViewModel extends BaseModel { + /// Constructor late CommentService _commentService; + + /// PostService instance. late PostService _postService; + + /// Post id on which comments are to be fetched. late String _postID; + + /// List of comments on the post. late List _commentlist; + + /// UserConfig instance. late UserConfig _userConfig; // Getters List get commentList => _commentlist; String get postId => _postID; - // initialiser. - Future initialise(String postID) async { + /// This function is used to initialise the CommentViewModel. + /// + /// To verify things are working, check out the native platform logs. + /// **params**: + /// * `postID`: The post id for which comments are to be fetched. + /// + /// **returns**: + /// * `Future`: promise that will be fulfilled message background activities are successful. + /// + Future initialise(String postID) async { _commentlist = []; _postID = postID; _commentService = locator(); @@ -37,12 +50,18 @@ class CommentsViewModel extends BaseModel { await getComments(); } - /// This methods fetch all comments on the post. - /// The function uses `getCommentsForPost` method by Comment Service. - Future getComments() async { + /// This function is used to get all comments on the post. + /// + /// To verify things are working, check out the native platform logs. + /// **params**: + /// None + /// + /// **returns**: + /// * `Future`: promise that will be fulfilled when comments are fetched. + /// + Future getComments() async { setState(ViewState.busy); - final List commentsJSON = - await _commentService.getCommentsForPost(_postID) as List; + final List commentsJSON = await _commentService.getCommentsForPost(_postID); print(commentsJSON); commentsJSON.forEach((commentJson) { _commentlist.add(Comment.fromJson(commentJson as Map)); @@ -50,18 +69,27 @@ class CommentsViewModel extends BaseModel { setState(ViewState.idle); } - /// This function add comment on the post. - /// The function uses `createComments` method provided by Comment Service. + /// This function add comment on the post. The function uses `createComments` method provided by Comment Service. + /// + /// **params**: + /// * `msg`: The comment text. /// - /// params: - /// * `msg` : text of the comment to add. - Future createComment(String msg) async { + /// **returns**: + /// * `Future`: promise that will be fulfilled when comment is added. + /// + Future createComment(String msg) async { print("comment viewModel called"); await _commentService.createComments(_postID, msg); addCommentLocally(msg); } - // This function add comment locally. + /// This function add comment locally. + /// + /// **params**: + /// * `msg`: BuildContext, contain parent info + /// + /// **returns**: + /// None void addCommentLocally(String msg) { _postService.addCommentLocally(_postID); final creator = _userConfig.currentUser; @@ -70,7 +98,7 @@ class CommentsViewModel extends BaseModel { createdAt: DateTime.now().toString(), creator: creator, ); - _commentlist.insert(0, localComment); + _commentlist.add(localComment); notifyListeners(); } } diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index a5557bb59c..4f51950a68 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -2822,14 +2822,25 @@ class MockCommentService extends _i2.Mock implements _i35.CommentService { ) as _i4.Future); @override - _i4.Future getCommentsForPost(String? postId) => (super.noSuchMethod( - Invocation.method( - #getCommentsForPost, - [postId], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i4.Future> getCommentsForPost(String? postId) { + final result = super.noSuchMethod( + Invocation.method( + #getCommentsForPost, + [postId], + ), + returnValue: _i4.Future>.value( + []), // Provide an empty list as a default value + returnValueForMissingStub: _i4.Future>.value([]), + ); + + // Check if the result is null, and return an empty list if it is + if (result == null) { + return _i4.Future>.value([]); + } + + // Otherwise, cast the result to List + return result as _i4.Future>; + } } /// A class which mocks [AppTheme]. diff --git a/test/service_tests/comment_service_test.dart b/test/service_tests/comment_service_test.dart index 0fa40c43fe..0792b6b934 100644 --- a/test/service_tests/comment_service_test.dart +++ b/test/service_tests/comment_service_test.dart @@ -1,6 +1,3 @@ -// ignore_for_file: talawa_api_doc -// ignore_for_file: talawa_good_doc_comments - import 'package:flutter_test/flutter_test.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:mockito/mockito.dart'; @@ -8,7 +5,7 @@ import 'package:talawa/locator.dart'; import 'package:talawa/services/comment_service.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/utils/comment_queries.dart'; - +import 'package:talawa/utils/post_queries.dart'; import '../helpers/test_helpers.dart'; void main() { @@ -48,63 +45,63 @@ void main() { }); test('test for getCommentsForPost', () async { final dataBaseMutationFunctions = locator(); - final String getCommmentQuery = - CommentQueries().getPostsComments('Ayush s postid'); + PostQueries().getPostById('Ayush s postid'); + when( dataBaseMutationFunctions.gqlAuthMutation(getCommmentQuery), ).thenAnswer( (_) async => QueryResult( options: QueryOptions(document: gql(getCommmentQuery)), data: { - 'commentsByPost': [ - { - 'creator': { - '_id': '123', - 'firstName': 'John', - 'lastName': 'Doe', - 'email': 'test@test.com', - }, - 'createdAt': '123456', - 'text': 'test text', - 'post': 'test post', - 'likeCount': 'test count', - }, - { - 'creator': { - '_id': '123', - 'firstName': 'Ayush', - 'lastName': 'Doe', - 'email': 'test@test.com', + 'post': { + 'comments': [ + { + 'creator': { + '_id': '123', + 'firstName': 'John', + 'lastName': 'Doe', + 'email': 'test@test.com', + }, + 'createdAt': '123456', + 'text': 'test text', + 'post': 'test post', + 'likeCount': 'test count', }, - 'createdAt': '123456', - 'text': 'test text', - 'post': 'test post', - 'likeCount': 'test count', - }, - { - 'creator': { - '_id': '123', - 'firstName': 'john', - 'lastName': 'chauhdary', - 'email': 'test@test.com', + { + 'creator': { + '_id': '123', + 'firstName': 'Ayush', + 'lastName': 'Doe', + 'email': 'test@test.com', + }, + 'createdAt': '123456', + 'text': 'test text', + 'post': 'test post', + 'likeCount': 'test count', }, - 'createdAt': '123456', - 'text': 'test text', - 'post': 'test post', - 'likeCount': 'test count', - } - ], + { + 'creator': { + '_id': '123', + 'firstName': 'john', + 'lastName': 'chauhdary', + 'email': 'test@test.com', + }, + 'createdAt': '123456', + 'text': 'test text', + 'post': 'test post', + 'likeCount': 'test count', + } + ], + }, }, source: QueryResultSource.network, ), ); final service = CommentService(); - final result = await service.getCommentsForPost('Ayush s postid'); - print(result); if (result.toString().contains('[{creator: ' '{' '_id: 123, ' @@ -141,22 +138,178 @@ void main() { final dataBaseMutationFunctions = locator(); final String getCommmentQuery = - CommentQueries().getPostsComments('Ayush'); + PostQueries().getPostById('Ayush s postid'); when( dataBaseMutationFunctions.gqlAuthMutation(getCommmentQuery), ).thenAnswer( (_) async => QueryResult( options: QueryOptions(document: gql(getCommmentQuery)), - data: null, + data: { + 'post': { + 'comments': [], + }, + }, + source: QueryResultSource.network, + ), + ); + + final service = CommentService(); + final result = await service.getCommentsForPost('Ayush postid'); + + if (result.toString().contains('[{creator: ' + '{' + '_id: 123, ' + 'firstName: John, ' + 'lastName: Doe, ' + 'email: test@test.com},' + ' createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: ' + '{_id: 123, ' + 'firstName: Ayush, ' + 'lastName: Doe, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: {_id: 123,' + ' firstName: john, ' + 'lastName: chauhdary, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}]')) { + fail('the result is not maatching'); + } + expect(result, isEmpty); + }); + + test('test for zero comments on post', () async { + final dataBaseMutationFunctions = locator(); + + final String getCommmentQuery = + PostQueries().getPostById('Ayush s postid'); + when( + dataBaseMutationFunctions.gqlAuthMutation(getCommmentQuery), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(getCommmentQuery)), + data: { + 'post': {'comments': []}, + }, source: QueryResultSource.network, ), ); final service = CommentService(); + final result = await service.getCommentsForPost('Ayush postid'); - final result = await service.getCommentsForPost('Ayush'); + if (result.toString().contains('[{creator: ' + '{' + '_id: 123, ' + 'firstName: John, ' + 'lastName: Doe, ' + 'email: test@test.com},' + ' createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: ' + '{_id: 123, ' + 'firstName: Ayush, ' + 'lastName: Doe, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: {_id: 123,' + ' firstName: john, ' + 'lastName: chauhdary, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}]')) { + fail('the result is not maatching'); + } + expect(result, isEmpty); + }); + + test('test when post is null', () async { + final dataBaseMutationFunctions = locator(); + + final String getCommmentQuery = + PostQueries().getPostById('Ayush s postid'); + when( + dataBaseMutationFunctions.gqlAuthMutation(getCommmentQuery), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(getCommmentQuery)), + data: { + 'post': null, + }, + source: QueryResultSource.network, + ), + ); + + final service = CommentService(); + final result = await service.getCommentsForPost('Ayush postid'); + + if (result.toString().contains('[{creator: ' + '{' + '_id: 123, ' + 'firstName: John, ' + 'lastName: Doe, ' + 'email: test@test.com},' + ' createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: ' + '{_id: 123, ' + 'firstName: Ayush, ' + 'lastName: Doe, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}, ' + '{creator: {_id: 123,' + ' firstName: john, ' + 'lastName: chauhdary, ' + 'email: test@test.com}, ' + 'createdAt: 123456, ' + 'text: test text, ' + 'post: test post, ' + 'likeCount: test count}]')) { + fail('the result is not maatching'); + } + expect(result, isEmpty); + }); + + test('test when result is null', () async { + final dataBaseMutationFunctions = locator(); + + final String getCommmentQuery = + PostQueries().getPostById('Ayush s postid'); + when( + dataBaseMutationFunctions.gqlAuthMutation(getCommmentQuery), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(getCommmentQuery)), + data: null, + source: QueryResultSource.network, + ), + ); + + final service = CommentService(); + final result = await service.getCommentsForPost('Ayush postid'); - print(result); if (result.toString().contains('[{creator: ' '{' '_id: 123, ' From bc36dac9cb677c5c32d0840995c76f9a97019fe2 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Sun, 24 Dec 2023 01:15:00 +0530 Subject: [PATCH 06/29] Node files not commit to project (#2267) * Fixed node file upload * Fixed node file upload --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index d0cd2ee334..b14badeec1 100644 --- a/.gitignore +++ b/.gitignore @@ -244,3 +244,12 @@ test/fixtures/core # Ignoring file that are generated during talawa testing and firebase initialization genhtml.perl test_img.png + + +# Ignoring Node files that are generated if user uses any node command which is not required for the project +node_modules/ +package.json +package-lock.json +yarn.lock +npm-debug.log +yarn-error.log \ No newline at end of file From c902baaf1467b985b5301a6b30a06d92516e4956 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:12:48 -0800 Subject: [PATCH 07/29] Update pull-request.yml - 88% coverage --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2099d2e9a0..6762757ced 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -123,7 +123,7 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v2 with: path: './coverage/lcov.info' - min_coverage: 87.0 + min_coverage: 88.0 Android-Build: name: Testing build for android From 2279ad74ccd5d27b8afbe8408858074e17cb21e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 04:58:46 -0800 Subject: [PATCH 08/29] Bump currency_picker from 2.0.19 to 2.0.20 (#2271) Bumps [currency_picker](https://github.com/Daniel-Ioannou/flutter_currency_picker) from 2.0.19 to 2.0.20. - [Release notes](https://github.com/Daniel-Ioannou/flutter_currency_picker/releases) - [Changelog](https://github.com/Daniel-Ioannou/flutter_currency_picker/blob/master/CHANGELOG.md) - [Commits](https://github.com/Daniel-Ioannou/flutter_currency_picker/compare/v2.0.19...V2.0.20) --- updated-dependencies: - dependency-name: currency_picker dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 28 ++++++++++++++-------------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b878a957d9..e13d24c967 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: currency_picker - sha256: "5b87c259dbdb4e032c6b9abd22158782868505b5217b453c6c36445612a3d34c" + sha256: eb75deb7bc92e3f31e1b8ad4efacf71371e8e49d7a0eebd1c1a8e9fae58cc23d url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.20" custom_lint: dependency: "direct dev" description: @@ -1049,10 +1049,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1518,18 +1518,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1597,10 +1597,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timelines: dependency: "direct main" description: @@ -1781,10 +1781,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1796bafc9d..44e0e69cbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: crypto: ^3.0.3 cupertino_icons: ^1.0.3 - currency_picker: ^2.0.19 + currency_picker: ^2.0.20 ############## Remove ########## # custom_lint_builder: ^0.4.0 From ac4082a82e9ca6ebcf5e0923238bbf65f4802430 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 04:59:03 -0800 Subject: [PATCH 09/29] Bump lint from 2.2.0 to 2.3.0 (#2270) Bumps [lint](https://github.com/passsy/dart-lint) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/passsy/dart-lint/releases) - [Changelog](https://github.com/passsy/dart-lint/blob/master/CHANGELOG.md) - [Commits](https://github.com/passsy/dart-lint/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: lint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e13d24c967..ac4fb77e9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1017,10 +1017,10 @@ packages: dependency: "direct dev" description: name: lint - sha256: "77b3777e8e9adca8e942da1e835882ae3248dfa00488a2ebbdbc1f1a4aa3f4a7" + sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 44e0e69cbf..c344336f29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,7 @@ dev_dependencies: hive_generator: ^2.0.1 json_serializable: ^6.7.1 - lint: ^2.2.0 + lint: ^2.3.0 mocktail_image_network: ^1.0.0 talawa_lint: From 97498441af6d2476c618731c0414280ce70f3981 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 04:59:24 -0800 Subject: [PATCH 10/29] Bump syncfusion_flutter_calendar from 23.2.7 to 24.1.41 (#2272) Bumps [syncfusion_flutter_calendar](https://github.com/syncfusion/flutter-widgets/tree/master/packages) from 23.2.7 to 24.1.41. - [Release notes](https://github.com/syncfusion/flutter-widgets/releases) - [Commits](https://github.com/syncfusion/flutter-widgets/commits/HEAD/packages) --- updated-dependencies: - dependency-name: syncfusion_flutter_calendar dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 12 ++++++------ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ac4fb77e9c..6f89532a1b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1550,26 +1550,26 @@ packages: dependency: "direct main" description: name: syncfusion_flutter_calendar - sha256: d57808a698c6ab1f0b79445822e84fdbcbb99ed7a6e33a4171a5265ad32f4b25 + sha256: ef1df99b0baf57c4b339d905a447633a8cbc612ac0e691ca0886591c9845fbb6 url: "https://pub.dev" source: hosted - version: "23.2.7" + version: "24.1.41" syncfusion_flutter_core: dependency: transitive description: name: syncfusion_flutter_core - sha256: a2427697bfad5b611db78ea4c4daef82d3350b83c729a8dc37959662a31547f9 + sha256: "69c827931957d5b121ee9f0b9b0b8d7d0d1ac537b61bcdd5c3fbffc044bbe86e" url: "https://pub.dev" source: hosted - version: "23.2.7" + version: "24.1.41" syncfusion_flutter_datepicker: dependency: "direct main" description: name: syncfusion_flutter_datepicker - sha256: b3340a7786f674d18bd22c226358648985e7631734dfc4aae09fdcfb71c09156 + sha256: "13c48582dc911663eac286791f009474fae964a731dc719d86ba0de8d8ade6f0" url: "https://pub.dev" source: hosted - version: "23.2.7" + version: "24.1.41" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c344336f29..b158cf4ef0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: shared_preferences: ^2.2.2 shimmer: ^3.0.0 social_share: ^2.2.1 - syncfusion_flutter_calendar: ^23.2.7 + syncfusion_flutter_calendar: ^24.1.41 syncfusion_flutter_datepicker: any timelines: ^0.1.0 tutorial_coach_mark: ^1.2.11 From 7b3a21f2534d6f05c79c1037dbc1bf598e2555c8 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Tue, 26 Dec 2023 01:49:20 +0530 Subject: [PATCH 11/29] Added test for access_request_view_model (#2273) --- .../access_request_view_model_test.dart | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 test/view_model_tests/access_request_view_model_test.dart diff --git a/test/view_model_tests/access_request_view_model_test.dart b/test/view_model_tests/access_request_view_model_test.dart new file mode 100644 index 0000000000..bd0352028e --- /dev/null +++ b/test/view_model_tests/access_request_view_model_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:talawa/constants/routing_constants.dart'; +import 'package:talawa/enums/enums.dart'; +import 'package:talawa/services/navigation_service.dart'; +import 'package:talawa/view_model/access_request_view_model.dart'; +import '../helpers/test_helpers.dart'; +import '../helpers/test_locator.dart'; + +class MockCallbackFunction extends Mock { + void call(); +} + +void main() { + testSetupLocator(); + + setUpAll(() { + registerServices(); + }); + + group( + "AccessScreenViewModel Test- ", + () { + test("Check if it's initialized correctly", () { + final org = userConfig.currentOrg; + final model = AccessScreenViewModel(); + expect(model.selectedOrganization.id, '-1'); + model.initialise(org); + expect(model.selectedOrganization.id, 'XYZ'); + }); + + test( + "Check if snackbar is showing in case of not empty joined organization", + () async { + final org = userConfig.currentOrg; + final model = AccessScreenViewModel(); + model.initialise(org); + + when( + databaseFunctions.gqlAuthMutation( + queries.sendMembershipRequest(org.id!), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions( + document: gql(queries.sendMembershipRequest(org.id!)), + ), + data: { + 'sendMembershipRequest': { + 'organization': { + '_id': 'XYZ', + 'name': 'Organization Name', + 'image': null, + 'description': null, + 'isPublic': false, + 'creator': { + 'firstName': 'ravidi', + 'lastName': 'shaikh', + 'image': null, + }, + }, + }, + }, + source: QueryResultSource.network, + ), + ); + + await model.sendMembershipRequest(); + + verify( + databaseFunctions.gqlAuthMutation( + queries.sendMembershipRequest(org.id!), + ), + ); + + verify(locator().pop()); + verify( + locator().showTalawaErrorSnackBar( + "Join in request sent to Organization Name successfully", + MessageType.info, + ), + ); + }); + + test("Removing joined organizations for testing", () async { + getAndRegisterUserConfig(); + final org = userConfig.currentOrg; + userConfig.currentUser.joinedOrganizations = []; + final model = AccessScreenViewModel(); + model.initialise(org); + + when( + databaseFunctions.gqlAuthMutation( + queries.sendMembershipRequest(org.id!), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions( + document: gql(queries.sendMembershipRequest(org.id!)), + ), + data: { + 'sendMembershipRequest': { + 'organization': { + '_id': 'XYZ', + 'name': 'Organization Name', + 'image': null, + 'description': null, + 'isPublic': false, + 'creator': { + 'firstName': 'ravidi', + 'lastName': 'shaikh', + 'image': null, + }, + }, + }, + }, + source: QueryResultSource.network, + ), + ); + + await model.sendMembershipRequest(); + + verify( + databaseFunctions.gqlAuthMutation( + queries.sendMembershipRequest(org.id!), + ), + ); + + verify( + locator().removeAllAndPush( + Routes.waitingScreen, + Routes.splashScreen, + ), + ); + }); + }, + ); +} From a7059f37c0b386d0afa02fa48da539f0c72c9950 Mon Sep 17 00:00:00 2001 From: Parag Gupta <103507835+Dante291@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:16:32 +0530 Subject: [PATCH 12/29] Enhancing UX by introducing User Profile Modification Feature (#2268) * Feature request: Implement User Profile Modification Feature * changes * changes * proper dynamic call * fixing tests * fixing coverage * writing test for missing lines * writing test for missing lines * writing test for missing lines * writing test for missing lines * writing test for missing lines * adding requested changes * adding requested changes * adding requested changes * adding requested changes * adding requested changes * adding requested changes * adding requested changes * test for missing lines * adding requested changes * adding requested changes * test for missing lines --- lang/de.json | 4 +- lang/en.json | 4 +- lang/es.json | 4 +- lang/fr.json | 4 +- lang/hi.json | 4 +- lang/ja.json | 4 +- lang/pt.json | 4 +- lang/zh.json | 4 +- .../add_post_view_model.dart | 3 + .../edit_profile_view_model.dart | 111 +++++- .../profile_page_view_model.dart | 2 +- .../select_organization_view_model.dart | 4 +- .../after_auth_screens/add_post_page.dart | 9 +- .../profile/edit_profile_page.dart | 160 +++++--- .../profile/profile_page.dart | 358 +++++++++--------- lib/widgets/post_widget.dart | 4 +- .../edit_profile_view_model_test.dart | 273 +++++++++++-- .../profile_page_view_model_test.dart | 2 +- .../profile/profile_page_test.dart | 67 +++- .../add_post_page_test.dart | 4 + .../profile/edit_profile_page_test.dart | 159 ++++++-- .../widgets/post_widget_test.dart | 2 +- 22 files changed, 898 insertions(+), 292 deletions(-) diff --git a/lang/de.json b/lang/de.json index 7ff5a6eea1..d903796ca1 100644 --- a/lang/de.json +++ b/lang/de.json @@ -162,5 +162,7 @@ "Notification Feature is not installed": "Meddelelsesfunktionen er ikke installeret", "For complete access, please": "Für vollständigen Zugriff bitte", " join an organization.": " einer Organisation beitreten.", - "JOIN":"BEITRETEN" + "JOIN":"BEITRETEN", + "Camera": "Kamera", + "Gallery": "Galerie" } diff --git a/lang/en.json b/lang/en.json index bed1b4a658..09bdd9211f 100644 --- a/lang/en.json +++ b/lang/en.json @@ -168,5 +168,7 @@ "No organizations found Please contact your admin": "No organizations found ! Please contact your admin", "For complete access, please": "For complete access, please", " join an organization.": " join an organization.", - "JOIN":"JOIN" + "JOIN":"JOIN", + "Camera": "Camera", + "Gallery": "Gallery" } diff --git a/lang/es.json b/lang/es.json index 5a8ffb18a3..d1060a09b5 100644 --- a/lang/es.json +++ b/lang/es.json @@ -163,5 +163,7 @@ "No organizations found Please contact your admin": "Neniuj organizoj trovitaj! Bonvolu kontakti vian administranton", "For complete access, please": "Para acceso completo, por favor", " join an organization.": " unirse a una organización.", - "JOIN":"UNIRSE" + "JOIN":"UNIRSE", + "Camera": "Cámara", + "Gallery": "Galería" } diff --git a/lang/fr.json b/lang/fr.json index 5070b7deb7..6db60cd069 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -165,5 +165,7 @@ "No organizations found Please contact your admin": "Aucune organisation trouvée ! Veuillez contacter votre administrateur", "For complete access, please": "Pour un accès complet, veuillez", " join an organization.": " rejoindre une organisation.", - "JOIN":"REJOINDRE" + "JOIN":"REJOINDRE", + "Camera": "Caméra", + "Gallery": "Galerie" } diff --git a/lang/hi.json b/lang/hi.json index db33287420..3e3ca23c4c 100644 --- a/lang/hi.json +++ b/lang/hi.json @@ -161,5 +161,7 @@ "No organizations found Please contact your admin": "कोई संगठन नहीं मिला! कृपया अपने व्यवस्थापक से संपर्क करें", "For complete access, please": "पूर्ण पहुंच के लिए, कृपया", " join an organization.": " किसी संगठन से जुड़ें.", - "JOIN":"जोड़ना" + "JOIN":"जोड़ना", + "Camera": "कैमरा", + "Gallery": "गैलरी" } diff --git a/lang/ja.json b/lang/ja.json index cf17112a0d..98b00fe3f8 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -164,5 +164,7 @@ "No organizations found Please contact your admin": "組織が見つかりません!管理者に連絡してください", "For complete access, please": "完全にアクセスするには、", " join an organization.": " 組織に参加します。", - "JOIN": "参加する" + "JOIN": "参加する", + "Camera": "カメラ", + "Gallery": "ギャラリー" } diff --git a/lang/pt.json b/lang/pt.json index 6948f0e918..1bd61aad6b 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -163,5 +163,7 @@ "No organizations found Please contact your admin": "Neniuj organizoj trovitaj! Bonvolu kontakti vian administranton", "For complete access, please": "Para acesso completo, por favor", " join an organization.": " ingressar em uma organização.", - "JOIN":"ENTRAR" + "JOIN":"ENTRAR", + "Camera": "Câmera", + "Gallery": "Galeria" } diff --git a/lang/zh.json b/lang/zh.json index 8d96d47899..ef0a204878 100644 --- a/lang/zh.json +++ b/lang/zh.json @@ -162,5 +162,7 @@ "No organizations found Please contact your admin": "Neniuj organizoj trovitaj! Bonvolu kontakti vian administranton", "For complete access, please": "如需完整访问,请", " join an organization.": " 加入一个组织。", - "JOIN":"加入" + "JOIN":"加入", + "Camera": "相机", + "Gallery": "画廊" } diff --git a/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart b/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart index 0bcc7ea1ad..eaed4aac3f 100644 --- a/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart +++ b/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart @@ -78,6 +78,9 @@ class AddPostViewModel extends BaseModel { String get userName => userConfig.currentUser.firstName! + userConfig.currentUser.lastName!; + /// User profile picture. + String? get userPic => userConfig.currentUser.image; + /// The organisation name. /// /// params: diff --git a/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart b/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart index b65a258320..20e83f6f03 100644 --- a/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart +++ b/lib/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:talawa/enums/enums.dart'; import 'package:talawa/locator.dart'; +import 'package:talawa/models/user/user_info.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; import 'package:talawa/view_model/base_view_model.dart'; @@ -16,6 +20,9 @@ class EditProfilePageViewModel extends BaseModel { /// profile image. late File? imageFile; + /// profile image in base64. + String? base64Image; + /// first name controller. TextEditingController firstNameTextController = TextEditingController(); @@ -52,7 +59,7 @@ class EditProfilePageViewModel extends BaseModel { /// /// **returns**: /// * `Future`: None - Future getImageFromGallery({bool camera = false}) async { + Future getImage({bool camera = false}) async { final image = await _multiMediaPickerService.getPhotoFromGallery(camera: camera); if (image != null) { @@ -61,6 +68,108 @@ class EditProfilePageViewModel extends BaseModel { } } + /// Method to select image from gallery or camera. + /// + /// **params**: + /// * `camera`: for true it will select from camera otherwise gallery + /// + /// **returns**: + /// * `Future`: none + Future selectImage({bool camera = false}) async { + if (camera) { + getImage(camera: true); + } else { + getImage(); + } + } + + /// This function is used to convert the image into Base64 format. + /// + /// **params**: + /// * `file`: Takes the image in format of file. + /// + /// **returns**: + /// * `Future`: image in string format + Future convertToBase64(File file) async { + try { + final List bytes = await file.readAsBytes(); + base64Image = base64Encode(bytes); + return base64Image!; + } catch (error) { + return ''; + } + } + + /// Method to update user profile. + /// + /// **params**: + /// * `firstName`: updated first name. + /// * `lastName`: updated last name. + /// * `newImage`: New profile picture that is to be updated. + /// + /// **returns**: + /// * `Future`: none + Future updateUserProfile({ + String? firstName, + String? lastName, + File? newImage, + }) async { + if (firstName == user.firstName && + newImage == null && + lastName == user.lastName) { + return; + } + try { + final Map variables = {}; + if (firstName != null) { + variables["firstName"] = firstName; + } + if (lastName != null) { + variables["lastName"] = lastName; + } + if (newImage != null) { + final String imageAsString = await convertToBase64(newImage); + variables["file"] = 'data:image/png;base64,$imageAsString'; + } + if (variables.isNotEmpty) { + await databaseService.gqlAuthMutation( + queries.updateUserProfile(), + variables: variables, + ); + // Fetch updated user info from the database and save it in hivebox. + final QueryResult result1 = await databaseFunctions.gqlAuthQuery( + queries.fetchUserInfo, + variables: {'id': user.id}, + ) as QueryResult; + final User userInfo = User.fromJson( + ((result1.data!['users'] as List)[0]) + as Map, + fromOrg: true, + ); + userInfo.authToken = userConfig.currentUser.authToken; + userInfo.refreshToken = userConfig.currentUser.refreshToken; + userConfig.updateUser(userInfo); + notifyListeners(); + + user.firstName = firstName ?? user.firstName; + user.lastName = lastName ?? user.lastName; + firstNameTextController.text = user.firstName!; + lastNameTextController.text = user.lastName!; + + navigationService.showTalawaErrorSnackBar( + "Profile updated successfully", + MessageType.info, + ); + notifyListeners(); + } + } on Exception catch (_) { + navigationService.showTalawaErrorSnackBar( + "Something went wrong", + MessageType.error, + ); + } + } + /// This function remove the selected image. /// /// **params**: diff --git a/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart b/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart index d17dadb471..9bbdb9330e 100644 --- a/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart +++ b/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart @@ -250,7 +250,7 @@ class ProfilePageViewModel extends BaseModel { void Function(void Function()) setter, ) { return InkWell( - key: const Key('dombtn1'), + key: Key('domBtn_$amount'), onTap: () { setter(() { donationAmount.text = amount; diff --git a/lib/view_model/pre_auth_view_models/select_organization_view_model.dart b/lib/view_model/pre_auth_view_models/select_organization_view_model.dart index 170da8b868..688b6878a0 100644 --- a/lib/view_model/pre_auth_view_models/select_organization_view_model.dart +++ b/lib/view_model/pre_auth_view_models/select_organization_view_model.dart @@ -259,9 +259,9 @@ class SelectOrganizationViewModel extends BaseModel { return { 'organizationsConnection': (existingOrganizations!["organizationsConnection"] - as List>) + + as List) + (newOrganizations!['organizationsConnection'] - as List>), + as List), }; }, ), diff --git a/lib/views/after_auth_screens/add_post_page.dart b/lib/views/after_auth_screens/add_post_page.dart index 7ed9fba9e8..028f751a9d 100644 --- a/lib/views/after_auth_screens/add_post_page.dart +++ b/lib/views/after_auth_screens/add_post_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:talawa/enums/enums.dart'; import 'package:talawa/locator.dart'; +import 'package:talawa/services/size_config.dart'; import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart'; import 'package:talawa/views/base_view.dart'; +import 'package:talawa/widgets/custom_avatar.dart'; /// Add Post View Model. late AddPostViewModel model; @@ -81,7 +83,12 @@ class _AddPostState extends State { child: Column( children: [ ListTile( - leading: const CircleAvatar(radius: 25), + leading: CustomAvatar( + isImageNull: model.userPic == null, + firstAlphabet: model.userName.substring(0, 1).toUpperCase(), + imageUrl: model.userPic, + fontSize: SizeConfig.screenHeight! * 0.018, + ), title: Text(model.userName), subtitle: Text( AppLocalizations.of(context)! diff --git a/lib/views/after_auth_screens/profile/edit_profile_page.dart b/lib/views/after_auth_screens/profile/edit_profile_page.dart index 05b74a1fc5..32de5599d7 100644 --- a/lib/views/after_auth_screens/profile/edit_profile_page.dart +++ b/lib/views/after_auth_screens/profile/edit_profile_page.dart @@ -22,21 +22,18 @@ class _EditProfilePageState extends State { return Scaffold( key: const Key('EditProfileScreenScaffold'), appBar: AppBar( - // returns a header for the page. backgroundColor: Theme.of(context).primaryColor, elevation: 0.0, title: Text( - // Title of the app bar(header). AppLocalizations.of(context)!.strictTranslate('Profile'), key: const Key('ProfileText'), style: Theme.of(context).textTheme.titleLarge!.copyWith( fontWeight: FontWeight.w600, - fontSize: 20, + fontSize: SizeConfig.screenHeight! * 0.03, ), ), ), body: SingleChildScrollView( - // SingleChildScrollView is a box in which a single widget can be scrolled. child: Column( children: [ SizedBox( @@ -47,69 +44,93 @@ class _EditProfilePageState extends State { children: [ // if the profile pic is not empty then render Circle Avatar with image as background image // else render Circle Avatar with grey background color. - model.imageFile != null - ? CircleAvatar( - radius: SizeConfig.screenHeight! * 0.082, - backgroundImage: Image.file( + CircleAvatar( + key: const Key('profilepic'), + radius: SizeConfig.screenHeight! * 0.082, + backgroundImage: model.imageFile != null + ? Image.file( model.imageFile!, fit: BoxFit.fitWidth, - ).image, - ) - : model.user.image != null - ? CircleAvatar( - key: const Key('UserImageInDb'), - radius: SizeConfig.screenHeight! * 0.082, - backgroundImage: - NetworkImage(model.user.image!), - ) - : CircleAvatar( - key: const Key('UserImageNotInDb'), - radius: SizeConfig.screenHeight! * 0.082, - backgroundColor: Colors.grey.withOpacity(0.2), - child: Text( - model.user.firstName! - .substring(0, 1) - .toUpperCase() + - model.user.lastName! - .substring(0, 1) - .toUpperCase(), + ).image + : model.user.image != null + ? NetworkImage(model.user.image!) + : null, + backgroundColor: + model.imageFile == null && model.user.image == null + ? Colors.grey.withOpacity(0.2) + : null, + child: model.imageFile == null + ? model.user.image == null + ? Text( + '${model.user.firstName![0].toUpperCase()}${model.user.lastName![0].toUpperCase()}', style: Theme.of(context) .textTheme .headlineMedium, - ), - ), + ) + : null + : null, + ), Positioned( bottom: 0, right: 0, child: InkWell( - // button to remove or set the profile image. key: const Key('AddRemoveImageButton'), onTap: () { - // if image is null the function will be get getImageFromGallery() - // else removeImage() + // modal sheet for image selection from camera or gallery. model.imageFile == null - ? model.getImageFromGallery(camera: true) + ? showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + height: + SizeConfig.screenHeight! * 0.135, + padding: const EdgeInsets.all(17), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _createModalSheetButton( + context, + Icons.camera_alt, + 'Camera', + () { + Navigator.of(context).pop(); + model.selectImage( + camera: true, + ); + }, + ), + _createModalSheetButton( + context, + Icons.photo_library, + 'Gallery', + () { + Navigator.of(context).pop(); + model.selectImage(); + }, + ), + ], + ), + ); + }, + ) : model.removeImage(); }, - child: model.imageFile == null - ? CircleAvatar( - radius: SizeConfig.screenHeight! * 0.034, - backgroundColor: - Theme.of(context).colorScheme.secondary, - child: const Icon( + child: CircleAvatar( + radius: SizeConfig.screenHeight! * 0.034, + backgroundColor: model.imageFile == null + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.secondary, + child: model.imageFile == null + ? const Icon( Icons.photo_camera, color: Colors.white, - ), - ) - : CircleAvatar( - radius: SizeConfig.screenHeight! * 0.02, - backgroundColor: - Theme.of(context).colorScheme.secondary, - child: const Icon( + ) + : const Icon( Icons.close, color: Colors.white, ), - ), + ), ), ), ], @@ -227,9 +248,16 @@ class _EditProfilePageState extends State { ), ), const Divider(), - // button to update the profile. TextButton( - onPressed: () {}, + key: const Key('updatebtn'), + onPressed: () { + model.updateUserProfile( + firstName: model.firstNameTextController.text, + newImage: model.imageFile, + lastName: model.lastNameTextController.text, + ); + FocusScope.of(context).unfocus(); + }, child: Text( AppLocalizations.of(context)!.strictTranslate('Update'), ), @@ -241,4 +269,36 @@ class _EditProfilePageState extends State { }, ); } + + /// Button for the different image selection methods. + /// + /// **params**: + /// * `context`:context for the sheet + /// * `icon`: icon for the method + /// * `label`: label for the method + /// * `onTap`: onTap funtionality for the method + /// + /// **returns**: + /// * `Widget`: Icon Button for selecting different image selection method. + Widget _createModalSheetButton( + BuildContext context, + IconData icon, + String label, + VoidCallback onTap, + ) { + return GestureDetector( + key: Key('select$label'), + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: SizeConfig.screenHeight! * 0.05, + ), + Text(AppLocalizations.of(context)!.strictTranslate(label)), + ], + ), + ); + } } diff --git a/lib/views/after_auth_screens/profile/profile_page.dart b/lib/views/after_auth_screens/profile/profile_page.dart index 5f8f2864ed..ab058478d8 100644 --- a/lib/views/after_auth_screens/profile/profile_page.dart +++ b/lib/views/after_auth_screens/profile/profile_page.dart @@ -51,21 +51,22 @@ class ProfilePage extends StatelessWidget { AppLocalizations.of(context)!.strictTranslate('Profile'), style: Theme.of(context).textTheme.titleLarge!.copyWith( // fontWeight: FontWeight.w600, - fontSize: 20, + fontSize: SizeConfig.screenHeight! * 0.03, fontFamily: 'open-sans', color: Colors.white, ), ), actions: [ IconButton( + key: const Key('settingIcon'), onPressed: () { showModalBottomSheet( context: context, builder: (BuildContext context) { return Container( - height: 200, + key: const Key('sheetContainer'), + height: SizeConfig.screenHeight! * 0.17, decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.only( bottomLeft: Radius.zero, bottomRight: Radius.zero, @@ -86,11 +87,14 @@ class ProfilePage extends StatelessWidget { child: const Text( "Edit Profile", style: TextStyle( - color: Colors.black38, fontFamily: 'open-sans', ), ), ), + Divider( + endIndent: SizeConfig.screenHeight! * 0.03, + indent: SizeConfig.screenHeight! * 0.03, + ), TextButton( onPressed: () { model.logout(context); @@ -98,7 +102,6 @@ class ProfilePage extends StatelessWidget { child: const Text( "Log Out", style: TextStyle( - color: Colors.black38, fontFamily: 'open-sans', ), ), @@ -118,191 +121,198 @@ class ProfilePage extends StatelessWidget { // else renders the widget. body: model.isBusy ? const CircularProgressIndicator() - : SingleChildScrollView( - child: Column( - children: [ - SizedBox( - height: SizeConfig.screenHeight! * 0.01, - ), - Row( - children: [ - Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CustomAvatar( - isImageNull: model.currentUser.image == null, - firstAlphabet: model.currentUser.firstName! - .substring(0, 1), - imageUrl: model.currentUser.image, - fontSize: Theme.of(context) - .textTheme - .titleLarge! - .fontSize, - maxRadius: 30, - ), - ), - ), - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${model.currentUser.firstName!} ${model.currentUser.lastName!}', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontFamily: 'open-sans', + : RefreshIndicator( + onRefresh: () async => model.initialize(), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: SizeConfig.screenHeight! * 0.01, + ), + Row( + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CustomAvatar( + key: const Key('profilepic'), + isImageNull: model.currentUser.image == null, + firstAlphabet: model.currentUser.firstName! + .substring(0, 1), + imageUrl: model.currentUser.image, + fontSize: Theme.of(context) + .textTheme + .titleLarge! + .fontSize, + maxRadius: SizeConfig.screenHeight! * 0.02, ), ), ), - ), - Expanded( - flex: 1, - child: IconButton( - icon: Icon( - Icons.share, - color: Theme.of(context).colorScheme.secondary, + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '${model.currentUser.firstName!} ${model.currentUser.lastName!}', + style: TextStyle( + color: Colors.white, + fontSize: SizeConfig.screenHeight! * 0.025, + fontFamily: 'open-sans', + ), + ), ), - onPressed: () => model.invite(context), ), - ), - ], - ), - const SizedBox( - height: 20, - ), - TalawaPluginProvider( - pluginName: "Donation", - visible: true, - child: Column( - children: [ - RaisedRoundedButton( - key: homeModel!.keySPDonateUs, - buttonLabel: - AppLocalizations.of(context)!.strictTranslate( - 'Donate to the Community', + Expanded( + flex: 1, + child: IconButton( + key: const Key('inviteicon'), + icon: Icon( + Icons.share, + color: + Theme.of(context).colorScheme.secondary, + ), + onPressed: () => model.invite(context), ), - onTap: () => donate(context, model), - textColor: Theme.of(context) - .inputDecorationTheme - .focusedBorder! - .borderSide - .color, - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, ), ], ), - ), - SizedBox( - height: 600, - width: double.infinity, - child: ContainedTabBarView( - tabs: [ - const Tab(text: 'Posts'), - const Tab(text: 'Events'), - const Tab(text: 'Tasks'), - ], - views: [ - ColoredBox( - color: Theme.of(context).colorScheme.background, - child: GridView.count( - mainAxisSpacing: 5, - crossAxisCount: 3, - children: [ - Image.asset('assets/images/pfp2.png'), - Image.asset('assets/images/pfp2.png'), - Image.asset('assets/images/pfp2.png'), - Image.asset('assets/images/pfp2.png'), - Image.asset('assets/images/pfp2.png'), - ], + SizedBox( + height: SizeConfig.screenHeight! * 0.02, + ), + TalawaPluginProvider( + pluginName: "Donation", + visible: true, + child: Column( + children: [ + RaisedRoundedButton( + key: homeModel!.keySPDonateUs, + buttonLabel: AppLocalizations.of(context)! + .strictTranslate( + 'Donate to the Community', + ), + onTap: () => donate(context, model), + textColor: Theme.of(context) + .inputDecorationTheme + .focusedBorder! + .borderSide + .color, + backgroundColor: Theme.of(context) + .colorScheme + .secondaryContainer, ), - ), - Container( - color: Theme.of(context).colorScheme.background, - ), - ColoredBox( - color: Theme.of(context).colorScheme.onPrimary, - child: GestureDetector( - onTap: () { - navigationService - .pushScreen(Routes.userTasks); - }, + ], + ), + ), + SizedBox( + height: SizeConfig.screenHeight! * 0.6, + width: double.infinity, + child: ContainedTabBarView( + tabs: [ + const Tab(text: 'Posts'), + const Tab(text: 'Events'), + const Tab(text: 'Tasks'), + ], + views: [ + ColoredBox( + color: Theme.of(context).colorScheme.background, + child: GridView.count( + mainAxisSpacing: 5, + crossAxisCount: 3, + children: [ + Image.asset('assets/images/pfp2.png'), + Image.asset('assets/images/pfp2.png'), + Image.asset('assets/images/pfp2.png'), + Image.asset('assets/images/pfp2.png'), + Image.asset('assets/images/pfp2.png'), + ], + ), ), - ), - ], + Container( + color: Theme.of(context).colorScheme.background, + ), + ColoredBox( + color: Theme.of(context).colorScheme.onPrimary, + child: GestureDetector( + key: const Key('tastscrn'), + onTap: () { + navigationService + .pushScreen(Routes.userTasks); + }, + ), + ), + ], + ), ), - ), - SizedBox( - height: SizeConfig.screenHeight! * 0.67, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - height: SizeConfig.screenHeight! * 0.01, - ), - SizedBox( - height: SizeConfig.screenHeight! * 0.05, - ), + SizedBox( + height: SizeConfig.screenHeight! * 0.67, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: SizeConfig.screenHeight! * 0.01, + ), + SizedBox( + height: SizeConfig.screenHeight! * 0.05, + ), - /// `Donation` acts as plugin. If visible is true the it will be always visible. - /// even if it's uninstalled by the admin (for development purposes) - //TODO: custom tile for Invitation. - CustomListTile( - key: homeModel!.keySPInvite, - index: 3, - type: TileType.option, - option: Options( - icon: Icon( - Icons.share, - color: - Theme.of(context).colorScheme.secondary, - size: 30, + /// `Donation` acts as plugin. If visible is true the it will be always visible. + /// even if it's uninstalled by the admin (for development purposes) + //TODO: custom tile for Invitation. + CustomListTile( + key: homeModel!.keySPInvite, + index: 3, + type: TileType.option, + option: Options( + icon: Icon( + Icons.share, + color: + Theme.of(context).colorScheme.secondary, + size: SizeConfig.screenHeight! * 0.025, + ), + // title + title: AppLocalizations.of(context)! + .strictTranslate('Invite'), + // subtitle + subtitle: AppLocalizations.of(context)! + .strictTranslate('Invite to org'), ), - // title - title: AppLocalizations.of(context)! - .strictTranslate('Invite'), - // subtitle - subtitle: AppLocalizations.of(context)! - .strictTranslate('Invite to org'), + // on tap call the invite function + onTapOption: () => model.invite(context), ), - // on tap call the invite function - onTapOption: () => model.invite(context), - ), - SizedBox( - height: SizeConfig.screenHeight! * 0.05, - ), - // Custom tile for Logout option. - //TODO: logout - // CustomListTile( - // key: homeModel!.keySPLogout, - // index: 3, - // type: TileType.option, - // option: Options( - // icon: Icon( - // Icons.logout, - // color: - // Theme.of(context).colorScheme.secondary, - // size: 30, - // ), - // title: AppLocalizations.of(context)! - // .strictTranslate('Log out'), - // subtitle: AppLocalizations.of(context)! - // .strictTranslate('Log out from Talawa'), - // ), - // // on tap calls the logout function - // onTapOption: () => model.logout(context), - // ), - SizedBox( - height: SizeConfig.screenHeight! * 0.05, - ), - FromPalisadoes(key: homeModel!.keySPPalisadoes), - ], + SizedBox( + height: SizeConfig.screenHeight! * 0.05, + ), + // Custom tile for Logout option. + //TODO: logout + // CustomListTile( + // key: homeModel!.keySPLogout, + // index: 3, + // type: TileType.option, + // option: Options( + // icon: Icon( + // Icons.logout, + // color: + // Theme.of(context).colorScheme.secondary, + // size: 30, + // ), + // title: AppLocalizations.of(context)! + // .strictTranslate('Log out'), + // subtitle: AppLocalizations.of(context)! + // .strictTranslate('Log out from Talawa'), + // ), + // // on tap calls the logout function + // onTapOption: () => model.logout(context), + // ), + SizedBox( + height: SizeConfig.screenHeight! * 0.05, + ), + FromPalisadoes(key: homeModel!.keySPPalisadoes), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/widgets/post_widget.dart b/lib/widgets/post_widget.dart index 10cd2681dd..7ec420a3ad 100644 --- a/lib/widgets/post_widget.dart +++ b/lib/widgets/post_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:talawa/models/post/post_model.dart'; +import 'package:talawa/services/graphql_config.dart'; import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/view_model/widgets_view_models/like_button_view_model.dart'; import 'package:talawa/views/base_view.dart'; @@ -53,7 +54,8 @@ class NewsPost extends StatelessWidget { isImageNull: post.creator!.image == null, firstAlphabet: post.creator!.firstName!.substring(0, 1).toUpperCase(), - imageUrl: post.creator!.image, + imageUrl: + "${'${GraphqlConfig.orgURI}'.replaceFirst('/graphql', '')}/${post.creator!.image}", fontSize: 20, ), title: Row( diff --git a/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/edit_profile_view_model_test.dart b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/edit_profile_view_model_test.dart index 112be39daa..d08d21490d 100644 --- a/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/edit_profile_view_model_test.dart +++ b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/edit_profile_view_model_test.dart @@ -3,67 +3,290 @@ import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:mockito/mockito.dart'; -import 'package:talawa/locator.dart'; +import 'package:talawa/enums/enums.dart'; +import 'package:talawa/services/size_config.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; import 'package:talawa/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart'; import '../../../helpers/test_helpers.dart'; +import '../../../helpers/test_locator.dart'; class MockCallbackFunction extends Mock { void call(); } void main() { - setUp(() { + testSetupLocator(); + SizeConfig().test(); + setUpAll(() { registerServices(); + graphqlConfig.test(); + sizeConfig.test(); + }); + + tearDownAll(() { + unregisterServices(); }); group('EditProfilePageViewModel Test -', () { test("Check if it's initialized correctly", () { final model = EditProfilePageViewModel(); model.initialize(); - expect(model.imageFile, null); }); + test('Profile shoud be edited if new values are given', () async { + final model = EditProfilePageViewModel(); + model.initialize(); + final Map mockData = { + 'updateUserProfile': { + '_id': '64378abd85008f171cf2990d', + }, + }; + final String a = await model.convertToBase64(File('path/to/newImage')); + final Map data = { + 'users': [ + { + '_id': '1234567890', + 'firstName': 'John', + 'lastName': 'Doe', + 'email': 'johndoe@example.com', + 'image': 'https://example.com/profile.jpg', + 'accessToken': 'exampleAccessToken', + 'refreshToken': 'exampleRefreshToken', + } + ], + }; + when( + databaseFunctions.gqlAuthMutation( + queries.updateUserProfile(), + variables: { + 'firstName': 'NewFirstName', + 'lastName': 'NewLastName', + 'newImage': 'data:image/png;base64,$a', + }, + ), + ).thenAnswer( + (_) async => QueryResult( + data: mockData, + source: QueryResultSource.network, + options: QueryOptions(document: gql(queries.updateUserProfile())), + ), + ); + when( + databaseFunctions.gqlAuthQuery( + queries.fetchUserInfo, + variables: {'id': model.user.id}, + ), + ).thenAnswer((_) async { + return QueryResult( + source: QueryResultSource.network, + data: data, + options: QueryOptions(document: gql(queries.fetchUserInfo)), + ); + }); + await model.updateUserProfile( + firstName: 'NewFirstName', + lastName: 'NewLastName', + newImage: File('path/to/newImage'), + ); - test( - 'Check if getImageFromGallery() is working fine when no image is return', - () async { - final notifyListenerCallback = MockCallbackFunction(); - final model = EditProfilePageViewModel() - ..addListener(notifyListenerCallback); + verify( + databaseFunctions.gqlAuthMutation( + queries.updateUserProfile(), + variables: { + "firstName": "NewFirstName", + "lastName": "NewLastName", + "file": 'data:image/png;base64,$a', + }, + ), + ).called(1); + verify( + navigationService.showTalawaErrorSnackBar( + "Profile updated successfully", + MessageType.info, + ), + ); + }); - when(locator().getPhotoFromGallery()) + test('Test UpdateUserProfile when throwing exception', () async { + final model = EditProfilePageViewModel(); + model.initialize(); + final String b = await model.convertToBase64(File('path/to/newIma')); + when( + databaseFunctions.gqlAuthMutation( + queries.updateUserProfile(), + variables: { + 'firstName': 'NewFirstNa', + 'lastName': 'NewLastNa', + 'newImage': 'data:image/png;base64,$b', + }, + ), + ).thenThrow(Exception()); + when( + databaseFunctions.gqlAuthQuery( + queries.fetchUserInfo, + variables: {'id': model.user.id}, + ), + ).thenThrow(Exception()); + await model.updateUserProfile( + firstName: 'NewFirstNa', + lastName: 'NewLastNa', + newImage: File('path/to/newIma'), + ); + verify( + navigationService.showTalawaErrorSnackBar( + "Something went wrong", + MessageType.error, + ), + ); + }); + testWidgets('Test if SelectImage from camera method works', + (WidgetTester tester) async { + final model = EditProfilePageViewModel(); + model.initialize(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + key: const Key('btn1'), + onPressed: () => model.selectImage(camera: true), + child: const Text('listner'), + ); + }, + ), + ), + ), + ); + final file = File('fakePath'); + when(locator().getPhotoFromGallery(camera: true)) .thenAnswer((realInvocation) async { - return null; + return file; }); + await tester.tap(find.byKey(const Key('btn1'))); + await tester.pumpAndSettle(); + verify(multimediaPickerService.getPhotoFromGallery(camera: true)) + .called(1); + expect(model.imageFile, file); + }); + testWidgets('Test if selectImage from gallery method works', + (WidgetTester tester) async { + final model = EditProfilePageViewModel(); model.initialize(); - await model.getImageFromGallery(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + key: const Key('btn1'), + onPressed: () => model.selectImage(), + child: const Text('listner'), + ); + }, + ), + ), + ), + ); + final file = File('fakePath'); + when(locator().getPhotoFromGallery()) + .thenAnswer((realInvocation) async { + return file; + }); + await tester.tap(find.byKey(const Key('btn1'))); + await tester.pumpAndSettle(); + expect(model.imageFile, file); + }); + testWidgets( + 'Test if SelectImage from camera method works if null is returned', + (WidgetTester tester) async { + final model = EditProfilePageViewModel(); + model.initialize(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + key: const Key('btn1'), + onPressed: () => model.selectImage(camera: true), + child: const Text('listner'), + ); + }, + ), + ), + ), + ); + when(locator().getPhotoFromGallery(camera: true)) + .thenAnswer((realInvocation) async { + return null; + }); + await tester.tap(find.byKey(const Key('btn1'))); + await tester.pumpAndSettle(); + verify(multimediaPickerService.getPhotoFromGallery(camera: true)) + .called(1); expect(model.imageFile, null); - verifyNever(notifyListenerCallback()); }); - - test('Check if getImageFromGallery() is working fine when iamge is return', - () async { - final notifyListenerCallback = MockCallbackFunction(); - final model = EditProfilePageViewModel() - ..addListener(notifyListenerCallback); - - final file = File('fakePath'); + testWidgets( + 'Test if selectImage from gallery method works when null is returned', + (WidgetTester tester) async { + final model = EditProfilePageViewModel(); + model.initialize(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + key: const Key('btn1'), + onPressed: () => model.selectImage(), + child: const Text('listner'), + ); + }, + ), + ), + ), + ); when(locator().getPhotoFromGallery()) .thenAnswer((realInvocation) async { - return file; + return null; }); + await tester.tap(find.byKey(const Key('btn1'))); + await tester.pumpAndSettle(); + expect(model.imageFile, null); + }); + test('No update performed if inputs are the same as existing data', + () async { + final model = EditProfilePageViewModel(); model.initialize(); - await model.getImageFromGallery(); + await model.updateUserProfile( + firstName: model.user.firstName, + lastName: model.user.lastName, + newImage: null, + ); + verifyNever( + databaseFunctions.gqlAuthMutation( + queries.updateUserProfile(), + variables: {'id': 'xzy1'}, + ), + ); + }); - expect(model.imageFile, file); - verify(notifyListenerCallback()).called(1); + test('convertToBase64 converts file to base64 string', () async { + final model = EditProfilePageViewModel(); + model.initialize(); + //using this asset as the test asset + final file = File('assets/images/Group 8948.png'); + final fileString = await model.convertToBase64(file); + expect(model.base64Image, fileString); }); test('Check if removeImage() is working fine', () async { diff --git a/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart index b4931c9df9..b674342905 100644 --- a/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart +++ b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart @@ -224,7 +224,7 @@ void main() async { ), ), ); - await tester.tap(find.byKey(const Key('dombtn1'))); + await tester.tap(find.byKey(const Key('domBtn_$amt'))); expect(setterCalled, true); final containerFinder = find.byType(Container); final Container container = tester.firstWidget(containerFinder); diff --git a/test/views/after_auth_screens/profile/profile_page_test.dart b/test/views/after_auth_screens/profile/profile_page_test.dart index 8a4e4861b9..64a7ced120 100644 --- a/test/views/after_auth_screens/profile/profile_page_test.dart +++ b/test/views/after_auth_screens/profile/profile_page_test.dart @@ -59,8 +59,6 @@ void main() async { await Hive.openBox('currentOrg'); final pbox = await Hive.openBox('pluginBox'); print(pbox.get('plugins')); - // locator.unregister(); - // locator.registerFactory(() => ProfilePageViewModel()); }); tearDownAll(() { @@ -69,14 +67,75 @@ void main() async { File('test/fixtures/core/currentuser.hive').delete(); File('test/fixtures/core/currentuser.lock').delete(); }); - testWidgets('check if profilePage shows up', (tester) async { - // print(); + testWidgets('check if profilePage shows up and refreshIndicator work', + (tester) async { await tester.pumpWidget( createProfilePage( mainScreenViewModel: locator(), ), ); + await tester.pump(); + expect(find.byType(RefreshIndicator), findsOneWidget); + await tester.drag( + find.byKey(const Key('profilepic')), + const Offset(0, 300), + ); + await tester.pumpAndSettle(); + }); + testWidgets('check if invitebutton work', (tester) async { + await tester.pumpWidget( + createProfilePage( + mainScreenViewModel: locator(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('inviteicon'))); + await tester.pumpAndSettle(); + }); + testWidgets('check if Donate button work', (tester) async { + await tester.pumpWidget( + createProfilePage( + mainScreenViewModel: locator(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Donate to the Community')); + await tester.pumpAndSettle(); + }); + testWidgets('check if naviagte to task screen work', (tester) async { + await tester.pumpWidget( + createProfilePage( + mainScreenViewModel: locator(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Tasks')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tastscrn'))); + await tester.pumpAndSettle(); + }); + testWidgets('check if Invite customListTile work', (tester) async { + await tester.pumpWidget( + createProfilePage( + mainScreenViewModel: locator(), + ), + ); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.text('Invite')); + await tester.tap(find.text('Invite')); + await tester.pumpAndSettle(); + }); + testWidgets('check if modal sheet for settings shows up', (tester) async { + await tester.pumpWidget( + createProfilePage( + mainScreenViewModel: locator(), + ), + ); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('settingIcon'))); + await tester.tap(find.byKey(const Key('settingIcon'))); await tester.pumpAndSettle(); + expect(find.byKey(const Key('sheetContainer')), findsOneWidget); }); }); } diff --git a/test/widget_tests/after_auth_screens/add_post_page_test.dart b/test/widget_tests/after_auth_screens/add_post_page_test.dart index f9f15117c2..b9984bcb71 100644 --- a/test/widget_tests/after_auth_screens/add_post_page_test.dart +++ b/test/widget_tests/after_auth_screens/add_post_page_test.dart @@ -32,6 +32,9 @@ class MockAddPostViewModel extends Mock implements AddPostViewModel { @override String get userName => 'UserName'; + @override + String? get userPic => userConfig.currentUser.image; + @override String get orgName => 'orgName'; @@ -88,6 +91,7 @@ void main() { // SizeConfig().test(); testSetupLocator(); // locator.registerSingleton(LikeButtonViewModel()); + sizeConfig.test(); setUp(() { registerServices(); diff --git a/test/widget_tests/after_auth_screens/profile/edit_profile_page_test.dart b/test/widget_tests/after_auth_screens/profile/edit_profile_page_test.dart index 24698a81ae..fb32846060 100644 --- a/test/widget_tests/after_auth_screens/profile/edit_profile_page_test.dart +++ b/test/widget_tests/after_auth_screens/profile/edit_profile_page_test.dart @@ -27,6 +27,9 @@ import '../../../helpers/test_locator.dart'; class MockBuildContext extends Mock implements BuildContext {} +class MockEditProfilePageViewModel extends Mock + implements EditProfilePageViewModel {} + class MockCallbackFunction extends Mock { void call(); } @@ -129,7 +132,7 @@ Future main() async { TalawaTheme.lightTheme.scaffoldBackgroundColor, ); final imageWidgetWithPicture = find.byKey( - const Key('UserImageNotInDb'), + const Key('profilepic'), ); expect(imageWidgetWithPicture, findsOneWidget); }); @@ -160,7 +163,7 @@ Future main() async { TalawaTheme.lightTheme.scaffoldBackgroundColor, ); final imageWidgetWithPicture = find.byKey( - const Key('UserImageInDb'), + const Key('profilepic'), ); expect(imageWidgetWithPicture, findsOneWidget); }); @@ -213,12 +216,20 @@ Future main() async { ); expect(appBarText, findsOneWidget); }); - testWidgets("Testing if Edit Screen shows image when not exist in database", + const Key('profilepic'); + testWidgets( + "Testing if Edit Screen shows image when already exist in database", (tester) async { await mockNetworkImages(() async { userConfig.updateUser(User()); + userConfig.updateUser( - User(firstName: 'Test', lastName: 'Test', email: 'test@test.com'), + User( + firstName: 'Test', + lastName: 'Test', + email: 'test@test.com', + image: 'https://via.placeholder.com/150', + ), ); await tester.pumpWidget(createChangePassScreenDark()); await tester.pumpAndSettle(); @@ -233,13 +244,12 @@ Future main() async { TalawaTheme.darkTheme.scaffoldBackgroundColor, ); final imageWidgetWithPicture = find.byKey( - const Key('UserImageNotInDb'), + const Key('profilepic'), ); expect(imageWidgetWithPicture, findsOneWidget); }); }); - testWidgets( - "Testing if Edit Screen shows image when already exist in database", + testWidgets("Testing if modalSheet appears when changing profile picture", (tester) async { await mockNetworkImages(() async { userConfig.updateUser(User()); @@ -264,10 +274,84 @@ Future main() async { .scaffoldBackgroundColor, TalawaTheme.darkTheme.scaffoldBackgroundColor, ); - final imageWidgetWithPicture = find.byKey( - const Key('UserImageInDb'), + await tester.tap(find.byKey(const Key('AddRemoveImageButton'))); + await tester.pumpAndSettle(); + expect(find.text('Camera'), findsOneWidget); + expect(find.text('Gallery'), findsOneWidget); + expect(find.byIcon(Icons.camera_alt), findsOneWidget); + expect(find.byIcon(Icons.photo_library), findsOneWidget); + }); + }); + testWidgets("Testing if image selection from camera work fine", + (tester) async { + await mockNetworkImages(() async { + userConfig.updateUser(User()); + + userConfig.updateUser( + User( + firstName: 'Test', + lastName: 'Test', + email: 'test@test.com', + image: 'https://via.placeholder.com/150', + ), ); - expect(imageWidgetWithPicture, findsOneWidget); + await tester.pumpWidget(createChangePassScreenDark()); + await tester.pumpAndSettle(); + final screenScaffoldWidget = find.byKey( + const Key('EditProfileScreenScaffold'), + ); + expect(screenScaffoldWidget, findsOneWidget); + expect( + (tester.firstWidget(find.byKey(const Key('Root'))) as MaterialApp) + .theme! + .scaffoldBackgroundColor, + TalawaTheme.darkTheme.scaffoldBackgroundColor, + ); + await tester.tap(find.byKey(const Key('AddRemoveImageButton'))); + await tester.pumpAndSettle(); + expect(find.text('Camera'), findsOneWidget); + expect(find.text('Gallery'), findsOneWidget); + expect(find.byIcon(Icons.camera_alt), findsOneWidget); + expect(find.byIcon(Icons.photo_library), findsOneWidget); + + await tester.ensureVisible(find.byIcon(Icons.camera_alt)); + await tester.tap(find.byIcon(Icons.camera_alt)); + }); + }); + testWidgets("Testing if image selection from gallery work fine", + (tester) async { + await mockNetworkImages(() async { + userConfig.updateUser(User()); + + userConfig.updateUser( + User( + firstName: 'Test', + lastName: 'Test', + email: 'test@test.com', + image: 'https://via.placeholder.com/150', + ), + ); + await tester.pumpWidget(createChangePassScreenDark()); + await tester.pumpAndSettle(); + final screenScaffoldWidget = find.byKey( + const Key('EditProfileScreenScaffold'), + ); + expect(screenScaffoldWidget, findsOneWidget); + expect( + (tester.firstWidget(find.byKey(const Key('Root'))) as MaterialApp) + .theme! + .scaffoldBackgroundColor, + TalawaTheme.darkTheme.scaffoldBackgroundColor, + ); + await tester.tap(find.byKey(const Key('AddRemoveImageButton'))); + await tester.pumpAndSettle(); + expect(find.text('Camera'), findsOneWidget); + expect(find.text('Gallery'), findsOneWidget); + expect(find.byIcon(Icons.camera_alt), findsOneWidget); + expect(find.byIcon(Icons.photo_library), findsOneWidget); + + await tester.ensureVisible(find.byIcon(Icons.photo_library)); + await tester.tap(find.byIcon(Icons.photo_library)); }); }); testWidgets("Testing if image selection and removal works", (tester) async { @@ -295,6 +379,30 @@ Future main() async { tester.tap(imageAvatar); }); }); + testWidgets("Testing Update butoon", (tester) async { + await mockNetworkImages(() async { + userConfig.updateUser(User()); + userConfig.updateUser( + User(firstName: 'Test', lastName: 'Test', email: 'test@test.com'), + ); + await tester.pumpWidget(createChangePassScreenDark()); + await tester.pumpAndSettle(); + final screenScaffoldWidget = find.byKey( + const Key('EditProfileScreenScaffold'), + ); + expect(screenScaffoldWidget, findsOneWidget); + expect( + (tester.firstWidget(find.byKey(const Key('Root'))) as MaterialApp) + .theme! + .scaffoldBackgroundColor, + TalawaTheme.darkTheme.scaffoldBackgroundColor, + ); + final updateButtonFinder = find.byKey(const Key('updatebtn')); + expect(updateButtonFinder, findsOneWidget); + await tester.tap(updateButtonFinder); + await tester.pumpAndSettle(); + }); + }); }); group('Testing image selection and removal in Edit Profile Screen', () { setUp(() { @@ -305,34 +413,39 @@ Future main() async { tearDown(() { unregisterServices(); }); - testWidgets('Testing image selection and removal in Edit Profile Screen', + + testWidgets( + 'Testing image selection when user is selecting image from device', (tester) async { final notifyListenerCallback = MockCallbackFunction(); final model = EditProfilePageViewModel() ..addListener(notifyListenerCallback); model.initialize(); - // testing getImageFromGallery - // with camera false - when(multimediaPickerService.getPhotoFromGallery(camera: false)) - .thenAnswer((realInvocation) async { - return null; - }); - - await model.getImageFromGallery(); - verify(multimediaPickerService.getPhotoFromGallery(camera: false)); - expect(model.imageFile, null); - // with camera true final file = File('fakePath'); when(multimediaPickerService.getPhotoFromGallery(camera: true)) .thenAnswer((_) async { return file; }); - await model.getImageFromGallery(camera: true); + await model.getImage(camera: true); verify(multimediaPickerService.getPhotoFromGallery(camera: true)); expect(model.imageFile, file); verify(notifyListenerCallback()); + await tester.pumpWidget(createChangePassScreenDark()); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('AddRemoveImageButton')), findsOneWidget); + await tester.tap(find.byKey(const Key('AddRemoveImageButton'))); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.camera_alt)); + await tester.pumpAndSettle(); + expect(model.imageFile, isNotNull); + }); + testWidgets('Testing if image removal work properly', (tester) async { + final notifyListenerCallback = MockCallbackFunction(); + final model = EditProfilePageViewModel() + ..addListener(notifyListenerCallback); + model.initialize(); // testing removeImage model.removeImage(); diff --git a/test/widget_tests/widgets/post_widget_test.dart b/test/widget_tests/widgets/post_widget_test.dart index 15d0af208d..20f2669281 100644 --- a/test/widget_tests/widgets/post_widget_test.dart +++ b/test/widget_tests/widgets/post_widget_test.dart @@ -273,7 +273,7 @@ void main() { // Testing props of Custom Avatar Widget expect(customAvatarWidget.isImageNull, true); - expect(customAvatarWidget.imageUrl, null); + expect(customAvatarWidget.imageUrl, ' /null'); expect(customAvatarWidget.fontSize, 20); expect(customAvatarWidget.firstAlphabet, 'T'); From 2a27c0a4a2b037271448ca787e05eacea9249d44 Mon Sep 17 00:00:00 2001 From: Shaik Azad <120930148+Azad99-9@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:27:32 +0530 Subject: [PATCH 13/29] Create ImageService (#2265) * resolved conflicts * fetchmore result typecast * added ImageService and written its tests * added coverage to missing lines. * Minor fix * fixed failing test * added ImageService and written its tests * resolved requested changes. * added changes to locator.dart * add debug print. * resolve requested changes. * resolved requested changes. --- lib/locator.dart | 11 +- lib/services/image_service.dart | 80 ++++++++++++ .../multi_media_pick_service.dart | 95 ++++++-------- .../add_post_view_model.dart | 29 +---- test/helpers/test_helpers.dart | 9 ++ test/helpers/test_locator.dart | 7 +- test/service_tests/image_service_test.dart | 101 +++++++++++++++ .../multi_media_pick_service_test.dart | 117 ++++++++---------- .../add_post_view_model_test.dart | 28 ++--- .../add_post_page_test.dart | 7 +- 10 files changed, 308 insertions(+), 176 deletions(-) create mode 100644 lib/services/image_service.dart create mode 100644 test/service_tests/image_service_test.dart diff --git a/lib/locator.dart b/lib/locator.dart index a980f45f3b..0c7dd4f3d7 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -8,6 +8,7 @@ import 'package:talawa/services/comment_service.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/services/event_service.dart'; import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/services/image_service.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/org_service.dart'; import 'package:talawa/services/post_service.dart'; @@ -70,11 +71,8 @@ final connectivity = locator(); ///creating GetIt for OrganizationService. final organizationService = locator(); -///creating GetIt for ImageCropper. -final imageCropper = locator(); - -///creating GetIt for ImagePicker. -final imagePicker = locator(); +///creating GetIt for ImageService. +final imageService = locator(); /// This function registers the widgets/objects in "GetIt". /// @@ -104,8 +102,9 @@ void setupLocator() { locator.registerLazySingleton(() => MultiMediaPickerService()); locator.registerLazySingleton(() => Connectivity()); locator.registerLazySingleton(() => ChatService()); - locator.registerLazySingleton(() => ImageCropper()); + locator.registerLazySingleton(() => ImageService()); locator.registerLazySingleton(() => ImagePicker()); + locator.registerLazySingleton(() => ImageCropper()); //graphql locator.registerSingleton(GraphqlConfig()); diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart new file mode 100644 index 0000000000..3d57e3ebfb --- /dev/null +++ b/lib/services/image_service.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:talawa/locator.dart'; + +/// ImageService class provides different functions as service in the context of Images. +/// +/// Services include: +/// * `cropImage` +/// * `convertToBase64` +class ImageService { + /// Global instance of ImageCropper. + final ImageCropper _imageCropper = locator(); + + /// Crops the image selected by the user. + /// + /// **params**: + /// * `imageFile`: the image file to be cropped. + /// + /// **returns**: + /// * `Future`: the image after been cropped. + /// + /// **throws**: + /// - `Exception`: If an error occurs during the image cropping process. + Future cropImage({required File imageFile}) async { + // try, to crop the image and returns a File with cropped image path. + try { + final CroppedFile? croppedImage = await _imageCropper.cropImage( + sourcePath: imageFile.path, + aspectRatioPresets: [ + CropAspectRatioPreset.square, + CropAspectRatioPreset.original, + ], + uiSettings: [ + AndroidUiSettings( + toolbarTitle: 'Crop Image', + toolbarColor: const Color(0xff18191A), + toolbarWidgetColor: Colors.white, + backgroundColor: Colors.black, + cropGridColor: Colors.white, + initAspectRatio: CropAspectRatioPreset.original, + lockAspectRatio: false, + ), + IOSUiSettings( + minimumAspectRatio: 1.0, + ), + ], + ); + + if (croppedImage != null) { + return File(croppedImage.path); + } + } catch (e) { + throw Exception( + "ImageService : $e.", + ); + } + + return null; + } + + /// Converts the image into Base64 format. + /// + /// **params**: + /// * `file`: Image as a File object. + /// + /// **returns**: + /// * `Future`: image in string format + Future convertToBase64(File file) async { + try { + final List bytes = await file.readAsBytes(); + final String base64String = base64Encode(bytes); + return base64String; + } catch (error) { + return null; + } + } +} diff --git a/lib/services/third_party_service/multi_media_pick_service.dart b/lib/services/third_party_service/multi_media_pick_service.dart index b793f4b2dc..616c0dc31c 100644 --- a/lib/services/third_party_service/multi_media_pick_service.dart +++ b/lib/services/third_party_service/multi_media_pick_service.dart @@ -6,12 +6,12 @@ Service usage: "add_post_view_model.dart" import 'dart:async'; import 'dart:io'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:talawa/locator.dart'; +import 'package:talawa/services/image_service.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/widgets/custom_alert_dialog.dart'; @@ -26,15 +26,22 @@ class MultiMediaPickerService { MultiMediaPickerService() { _picker = locator(); _fileStream = _fileStreamController.stream.asBroadcastStream(); + _imageService = imageService; } - //Local Variables + /// Controller for handling the stream of selected files. final StreamController _fileStreamController = StreamController(); + + /// Stream of selected files. late Stream _fileStream; + + /// [ImagePicker] used for selecting images or videos. late ImagePicker _picker; - //Getters - /// This function returns the stream of files. + /// [ImageService] for additional image-related operations. + late ImageService _imageService; + + /// Provides a stream of selected multimedia files. /// /// params: /// None. @@ -43,11 +50,10 @@ class MultiMediaPickerService { /// * `Stream`: Stream of files. Stream get fileStream => _fileStream; - /// This function is used to pick the image from gallery or to click the image from user's camera. - /// - /// The function first ask for the permission to access the camera, if denied then returns a message in. + /// Picks the image from gallery or to click the image from user's camera. /// - /// custom Dialog Box. This function returns a File type for which `camera` variable is false by default. + /// First ask for the permission to access the camera, if denied then returns a message in. + /// custom Dialog Box. Returns a File type for which `camera` variable is false by default. /// /// **params**: /// * `camera`: if true then open camera for image, else open gallery to select image. @@ -63,73 +69,46 @@ class MultiMediaPickerService { ); // if image is selected or not null, call the cropImage function that provide service to crop the selected image. if (image != null) { - return await cropImage(imageFile: File(image.path)); + return await _imageService.cropImage( + imageFile: File(image.path), + ); } } catch (e) { // if the permission denied or error occurs. if (e is PlatformException && e.code == 'camera_access_denied') { // push the dialog alert with the message. locator().pushDialog( - CustomAlertDialog( - success: () { - locator().pop(); - openAppSettings(); - }, - dialogTitle: 'Permission Denied', - successText: 'SETTINGS', - dialogSubTitle: - "Camera permission is required, to use this feature, give permission from app settings", - ), + permissionDeniedDialog(), ); } - print( + debugPrint( "MultiMediaPickerService : Exception occurred while choosing photo from the gallery $e", ); } + return null; } - /// This function is used to crop the image selected by the user. + /// Generates a custom alert dialog for permission denial. /// - /// The function accepts a `File` type image and returns `File` type of cropped image. + /// When called, it creates and returns a `CustomAlertDialog` widget with pre-defined settings. + /// This dialog prompts the user to grant camera permissions from the app settings. /// /// **params**: - /// * `imageFile`: the image file to be cropped. + /// None /// /// **returns**: - /// * `Future`: the image after been cropped. - Future cropImage({required File imageFile}) async { - // try, to crop the image and returns a File with cropped image path. - try { - final CroppedFile? croppedImage = await locator().cropImage( - sourcePath: imageFile.path, - aspectRatioPresets: [ - CropAspectRatioPreset.square, - CropAspectRatioPreset.original, - ], - uiSettings: [ - AndroidUiSettings( - toolbarTitle: 'Crop Image', - toolbarColor: const Color(0xff18191A), - toolbarWidgetColor: Colors.white, - backgroundColor: Colors.black, - cropGridColor: Colors.white, - initAspectRatio: CropAspectRatioPreset.original, - lockAspectRatio: false, - ), - IOSUiSettings( - minimumAspectRatio: 1.0, - ), - ], - ); - if (croppedImage != null) { - return File(croppedImage.path); - } - } catch (e) { - print( - "MultiMediaPickerService : Exception occurred while cropping Image", - ); - } - return null; + /// * `CustomAlertDialog`: Custom Alert Dialog widget. + CustomAlertDialog permissionDeniedDialog() { + return CustomAlertDialog( + success: () { + locator().pop(); + openAppSettings(); + }, + dialogTitle: 'Permission Denied', + successText: 'SETTINGS', + dialogSubTitle: + "Camera permission is required, to use this feature, give permission from app settings", + ); } } diff --git a/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart b/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart index eaed4aac3f..af53cb5dda 100644 --- a/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart +++ b/lib/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -7,6 +6,7 @@ import 'package:talawa/enums/enums.dart'; import 'package:talawa/locator.dart'; import 'package:talawa/models/organization/org_info.dart'; import 'package:talawa/services/database_mutation_functions.dart'; +import 'package:talawa/services/image_service.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; import 'package:talawa/services/user_config.dart'; @@ -22,6 +22,7 @@ class AddPostViewModel extends BaseModel { //Services late MultiMediaPickerService _multiMediaPickerService; late NavigationService _navigationService; + late ImageService _imageService; // ignore: unused_field late File? _imageFile; @@ -65,7 +66,7 @@ class AddPostViewModel extends BaseModel { /// **returns**: /// * `Future`: define_the_return Future setImageInBase64(File file) async { - _imageInBase64 = await convertToBase64(file); + _imageInBase64 = await _imageService.convertToBase64(file); notifyListeners(); } @@ -124,33 +125,15 @@ class AddPostViewModel extends BaseModel { void initialise() { _navigationService = locator(); _imageFile = null; + _imageInBase64 = null; _multiMediaPickerService = locator(); + _imageService = locator(); if (!demoMode) { _dbFunctions = locator(); _selectedOrg = locator().currentOrg; } } - /// to convert the image in base64. - /// - /// - /// **params**: - /// * `file`: file of image clicked. - /// - /// **returns**: - /// * `Future`: Future string containing the base 64 format image - Future convertToBase64(File file) async { - try { - final List bytes = await file.readAsBytes(); - final String base64String = base64Encode(bytes); - print(base64String); - _imageInBase64 = base64String; - return base64String; - } catch (error) { - return ''; - } - } - /// This function is used to get the image from gallery. /// /// The function uses the `_multiMediaPickerService` services. @@ -167,7 +150,7 @@ class AddPostViewModel extends BaseModel { if (image != null) { _imageFile = image; // convertImageToBase64(image.path); - convertToBase64(image); + _imageInBase64 = await _imageService.convertToBase64(image); // print(_imageInBase64); _navigationService.showTalawaErrorSnackBar( "Image is added", diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 4fca029420..392d273c44 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -25,6 +25,7 @@ import 'package:talawa/services/comment_service.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/services/event_service.dart'; import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/services/image_service.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/org_service.dart'; import 'package:talawa/services/post_service.dart'; @@ -53,6 +54,7 @@ import 'package:talawa/view_model/theme_view_model.dart'; import 'package:talawa/view_model/widgets_view_models/custom_drawer_view_model.dart'; import 'package:talawa/view_model/widgets_view_models/like_button_view_model.dart'; import 'package:talawa/view_model/widgets_view_models/progress_dialog_view_model.dart'; +import '../service_tests/image_service_test.dart'; import '../views/main_screen_test.dart'; import 'test_helpers.mocks.dart'; @@ -429,6 +431,13 @@ ImageCropper getAndRegisterImageCropper() { return service; } +ImageService getAndRegisterImageService() { + _removeRegistrationIfExists(); + final service = MockImageService(); + locator.registerLazySingleton(() => service); + return service; +} + ImagePicker getAndRegisterImagePicker() { _removeRegistrationIfExists(); final service = MockImagePicker(); diff --git a/test/helpers/test_locator.dart b/test/helpers/test_locator.dart index 1cfc71fddf..f11dcb1d90 100644 --- a/test/helpers/test_locator.dart +++ b/test/helpers/test_locator.dart @@ -9,6 +9,7 @@ import 'package:talawa/services/comment_service.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/services/event_service.dart'; import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/services/image_service.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/org_service.dart'; import 'package:talawa/services/post_service.dart'; @@ -55,8 +56,9 @@ final eventService = locator(); final commentsService = locator(); final postService = locator(); final mainScreenViewModel = locator(); -final imageCropper = locator(); +final imageService = locator(); final imagePicker = locator(); +final imageCropper = locator(); void testSetupLocator() { //services @@ -74,8 +76,9 @@ void testSetupLocator() { locator.registerLazySingleton(() => EventService()); locator.registerLazySingleton(() => CommentService()); locator.registerLazySingleton(() => MultiMediaPickerService()); - locator.registerLazySingleton(() => ImageCropper()); + locator.registerLazySingleton(() => ImageService()); locator.registerLazySingleton(() => ImagePicker()); + locator.registerLazySingleton(() => ImageCropper()); locator.registerSingleton(() => OrganizationService()); //graphql diff --git a/test/service_tests/image_service_test.dart b/test/service_tests/image_service_test.dart new file mode 100644 index 0000000000..cce18ff630 --- /dev/null +++ b/test/service_tests/image_service_test.dart @@ -0,0 +1,101 @@ +// ignore_for_file: talawa_api_doc +// ignore_for_file: talawa_good_doc_comments + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:mockito/mockito.dart'; +import 'package:talawa/services/image_service.dart'; + +import '../helpers/test_helpers.dart'; +import '../helpers/test_locator.dart'; + +class MockImageService extends Mock implements ImageService { + @override + Future convertToBase64(File file) async { + return ""; + } +} + +void main() { + testSetupLocator(); + + setUpAll(() { + registerServices(); + }); + + group('Tests for Crop Image', () { + test("test no image provided for the image cropper", () async { + const path = 'test'; + final file = await imageService.cropImage(imageFile: File(path)); + expect(file?.path, null); + }); + + test("crop image method", () async { + final mockImageCropper = imageCropper; + + const path = "test"; + final fakefile = File(path); + final croppedFile = CroppedFile("fakeCropped"); + + when( + mockImageCropper.cropImage( + sourcePath: "test", + aspectRatioPresets: [ + CropAspectRatioPreset.square, + CropAspectRatioPreset.original, + ], + uiSettings: anyNamed('uiSettings'), + ), + ).thenAnswer((realInvocation) async => croppedFile); + + final file = await imageService.cropImage(imageFile: fakefile); + + expect(file?.path, croppedFile.path); + }); + + test("error in crop image", () async { + final mockImageCropper = locator(); + const path = "test"; + final fakefile = File(path); + when( + mockImageCropper.cropImage( + sourcePath: "test", + aspectRatioPresets: [ + CropAspectRatioPreset.square, + CropAspectRatioPreset.original, + ], + uiSettings: anyNamed('uiSettings'), + ), + ).thenThrow(Exception()); + expect( + imageService.cropImage(imageFile: fakefile), + throwsException, + ); + }); + }); + + group('Tests for convertToBase64', () { + test('convertToBase64 converts file to base64 string', () async { + //using this asset as the test asset + final file = File('assets/images/Group 8948.png'); + final List encodedBytes = file.readAsBytesSync(); + + final fileString = await imageService.convertToBase64(file); + + final List decodedBytes = base64Decode(fileString!); + + expect(decodedBytes, equals(encodedBytes)); + }); + + test( + 'Check if convertToBase64 is working even if wrong file path is provided', + () async { + final file = File('fakePath'); + final fileString = await imageService.convertToBase64(file); + expect(null, fileString); + }); + }); +} diff --git a/test/service_tests/multi_media_pick_service_test.dart b/test/service_tests/multi_media_pick_service_test.dart index d1f887d9f0..1205bc5099 100644 --- a/test/service_tests/multi_media_pick_service_test.dart +++ b/test/service_tests/multi_media_pick_service_test.dart @@ -2,50 +2,30 @@ // ignore_for_file: talawa_good_doc_comments import 'dart:async'; -import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mockito/mockito.dart'; -import 'package:talawa/locator.dart'; +import 'package:talawa/services/navigation_service.dart'; +import 'package:talawa/services/size_config.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; +import 'package:talawa/utils/app_localization.dart'; import '../helpers/test_helpers.dart'; +import '../helpers/test_locator.dart'; void main() { + testSetupLocator(); setUp(() { registerServices(); }); + tearDown(() { + unregisterServices(); + }); + SizeConfig().test(); group('MultiMediaPickerService test', () { - test("test get fileStream", () async { - final model = MultiMediaPickerService(); - expect( - model.fileStream.toString(), - "Instance of '_AsBroadcastStream'", - ); - }); - test("crop image method", () async { - final mockImageCropper = imageCropper; - final model = MultiMediaPickerService(); - - const path = "test"; - final fakefile = File(path); - final croppedFile = CroppedFile("fakeCropped"); - - when( - mockImageCropper.cropImage( - sourcePath: "test", - aspectRatioPresets: [ - CropAspectRatioPreset.square, - CropAspectRatioPreset.original, - ], - uiSettings: anyNamed('uiSettings'), - ), - ).thenAnswer((realInvocation) async => croppedFile); - final file = await model.cropImage(imageFile: fakefile); - // verify(mockImageCropper.cropImage(sourcePath: fakefile.path)); - expect(file?.path, croppedFile.path); - }); test("test get photo from gallery method if camera option is false", () async { final mockImageCropper = locator(); @@ -99,12 +79,6 @@ void main() { final file = await model.getPhotoFromGallery(camera: false); expect(file?.path, null); }); - test("test no image provided for the image cropper", () async { - final model = MultiMediaPickerService(); - const path = 'test'; - final file = await model.cropImage(imageFile: File(path)); - expect(file?.path, null); - }); test("camera access denied", () async { final mockPicker = locator(); final model = MultiMediaPickerService(); @@ -127,35 +101,50 @@ void main() { "MultiMediaPickerService : Exception occurred while choosing photo from the gallery $error", ); }); - test("error in crop image", () async { - final mockImageCropper = locator(); - final model = MultiMediaPickerService(); - const path = "test"; - final fakefile = File(path); - final printed = []; - when( - mockImageCropper.cropImage( - sourcePath: "test", - aspectRatioPresets: [ - CropAspectRatioPreset.square, - CropAspectRatioPreset.original, - ], - uiSettings: anyNamed('uiSettings'), - ), - ).thenThrow(Exception()); - runZoned( - () async { - await model.cropImage(imageFile: fakefile); - }, - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) { - printed.add(line); - }, - ), + + testWidgets('Test for permission_denied_dialog success action.', + (tester) async { + final service = MultiMediaPickerService(); + + final Widget app = MaterialApp( + navigatorKey: locator().navigatorKey, + navigatorObservers: [], + locale: const Locale('en'), + supportedLocales: [ + const Locale('en', 'US'), + const Locale('es', 'ES'), + const Locale('fr', 'FR'), + const Locale('hi', 'IN'), + const Locale('zh', 'CN'), + const Locale('de', 'DE'), + const Locale('ja', 'JP'), + const Locale('pt', 'PT'), + ], + localizationsDelegates: [ + const AppLocalizationsDelegate(isTest: true), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + home: Scaffold(body: service.permissionDeniedDialog()), ); + + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + final settingsFinder = find.textContaining('SETTINGS'); + + expect(settingsFinder, findsOneWidget); + + await tester.tap(settingsFinder); + + verify(navigationService.pop()); + }); + + test("test get fileStream", () async { + final model = MultiMediaPickerService(); expect( - printed[0], - "MultiMediaPickerService : Exception occurred while cropping Image", + model.fileStream.toString(), + "Instance of '_AsBroadcastStream'", ); }); }); diff --git a/test/view_model_tests/after_auth_view_model_tests/add_post_view_model_test.dart b/test/view_model_tests/after_auth_view_model_tests/add_post_view_model_test.dart index 1c89d1daba..227eb914e8 100644 --- a/test/view_model_tests/after_auth_view_model_tests/add_post_view_model_test.dart +++ b/test/view_model_tests/after_auth_view_model_tests/add_post_view_model_test.dart @@ -23,6 +23,7 @@ void main() { testSetupLocator(); setUp(() { registerServices(); + getAndRegisterImageService(); }); group("AddPostViewModel Test - ", () { test("Check if it's initialized correctly", () { @@ -36,6 +37,13 @@ void main() { userConfig.currentUser.firstName! + userConfig.currentUser.lastName!, ); }); + + test('Test for imageInBase64 getter', () async { + final model = AddPostViewModel(); + model.initialise(); + expect(model.imageInBase64, null); + }); + test("Check if getImageFromGallery() is working fine", () async { final model = AddPostViewModel(); model.initialise(); @@ -189,25 +197,5 @@ void main() { model.removeImage(); expect(model.imageFile, null); }); - test('convertToBase64 converts file to base64 string', () async { - final notifyListenerCallback = MockCallbackFunction(); - final model = AddPostViewModel()..addListener(notifyListenerCallback); - model.initialise(); - //using this asset as the test asset - final file = File('assets/images/Group 8948.png'); - final fileString = await model.convertToBase64(file); - expect(model.imageInBase64, fileString); - }); - - test( - 'Check if convertToBase64 is working even if wrong file path is provided', - () async { - final notifyListenerCallback = MockCallbackFunction(); - final model = AddPostViewModel()..addListener(notifyListenerCallback); - model.initialise(); - final file = File('fakePath'); - final fileString = await model.convertToBase64(file); - expect('', fileString); - }); }); } diff --git a/test/widget_tests/after_auth_screens/add_post_page_test.dart b/test/widget_tests/after_auth_screens/add_post_page_test.dart index b9984bcb71..2d6d3c9e1c 100644 --- a/test/widget_tests/after_auth_screens/add_post_page_test.dart +++ b/test/widget_tests/after_auth_screens/add_post_page_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:talawa/locator.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart'; @@ -14,7 +15,6 @@ import 'package:talawa/view_model/main_screen_view_model.dart'; import 'package:talawa/views/after_auth_screens/add_post_page.dart'; import '../../helpers/test_helpers.dart'; -import '../../helpers/test_locator.dart'; final homeModel = locator(); bool removeImageCalled = false; @@ -89,12 +89,13 @@ Widget createAddPostScreen({ void main() { // SizeConfig().test(); - testSetupLocator(); + setupLocator(); // locator.registerSingleton(LikeButtonViewModel()); sizeConfig.test(); setUp(() { registerServices(); + getAndRegisterImageService(); }); group('createAddPostScreen Test', () { @@ -277,7 +278,7 @@ void main() { }); await tester.tap(finder); - await tester.pump(); + await tester.pumpAndSettle(); await tester.tap(cancelBtn); await tester.pump(); From c8c4cabe705dfd3309d5388514d182e44a20f0cd Mon Sep 17 00:00:00 2001 From: Parag Gupta <103507835+Dante291@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:22:16 +0530 Subject: [PATCH 14/29] Streamlining CI/CD workflow for iOS Build and Release Process in Talawa (#2266) * testing automation * testing automation * Streamlining CI/CD workflow for iOS Build and Release Process in Talawa * Streamlining CI/CD workflow for iOS Build and Release Process in Talawa * changes * changes * changes --- .github/workflows/push.yaml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 4e1c64ab6d..2a4dd0ae1b 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -223,7 +223,7 @@ jobs: This may or may not be stable, so please have a look at the stable release(s). iOS-Build: - name: Testing build for iOS + name: iOS Build and Relaese runs-on: macos-latest needs: Flutter-Testing steps: @@ -235,3 +235,27 @@ jobs: architecture: x64 - name: Building for ios run: flutter build ios --release --no-codesign + # '--no-codesign' is used for building without code signing. + # For actual distribution, proper code signing is required. + + ######################################################## + ## Package the app as an .ipa and create a release ## + ######################################################## + + - name: Releasing for iOS + run: | + mkdir Payload + cp -r build/ios/iphoneos/Runner.app Payload/Runner.app + zip -r app.ipa Payload + # This packages the Runner.app into an .ipa file + + - uses: ncipollo/release-action@v1 + with: + name: "Automated iOS Release" + artifacts: "app-release.ipa" + allowUpdates: "true" + generateReleaseNotes: false + tag: "automated" + body: | + This is an automated release, triggered by a recent push. + This may or may not be stable, so please have a look at the stable release(s). From 1172c51919d53b225343abd4d15e105b2cd8df11 Mon Sep 17 00:00:00 2001 From: ANKIT VARSHNEY <132201033+AVtheking@users.noreply.github.com> Date: Fri, 29 Dec 2023 02:41:59 +0530 Subject: [PATCH 15/29] added check for all translation file (#2275) * added check for all translation file * fix falling test * fix the python style guide * made the requested changes * now the script will report for every file if there is error * revert back the double quotes to single quotes * error message will show path of file * improved the output message * used namedtuple to improve readability --- .github/workflows/compare_translations.py | 119 ++++++++++++++++++++++ .github/workflows/pull-request.yml | 27 +++-- pubspec.lock | 44 ++++++-- 3 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/compare_translations.py diff --git a/.github/workflows/compare_translations.py b/.github/workflows/compare_translations.py new file mode 100644 index 0000000000..1799240d95 --- /dev/null +++ b/.github/workflows/compare_translations.py @@ -0,0 +1,119 @@ +""" +Script to encourage more efficient coding practices. + +Methodology: + + Utility for comparing translations between default and other languages. + + This module defines a function to compare two translations + and print any missing keys in the other language's translation. +Attributes: + FileTranslation (namedtuple): Named tuple to represent a combination of file and missing translations. + Fields: + - file (str): The file name. + - missing_translations (list): List of missing translations. + +Functions: + compare_translations(default_translation, other_translation): + Compare two translations and print missing keys. + + check_translations(): + Load the default translation and compare it with other translations. + +Usage: + This script can be executed to check and print missing + translations in other languages based on the default English translation. + +Example: + python compare_translations.py +NOTE: + This script complies with our python3 coding and documentation standards + and should be used as a reference guide. It complies with: + + 1) Pylint + 2) Pydocstyle + 3) Pycodestyle + 4) Flake8 + +""" +# standard imports +import json +import os +import sys +from collections import namedtuple + +# Named tuple for file and missing translations combination +FileTranslation = namedtuple("FileTranslation", ["file", "missing_translations"]) + + +def compare_translations(default_translation, other_translation): + """Compare two translations and print missing keys. + + Args: + default_translation: The default translation + other_translation: The other translation + + Returns: + missing_translations: List of missing translations + """ + missing_translations = [] + + for key in default_translation: + if key not in other_translation: + missing_translations.append(key) + + return missing_translations + + +def load_translation(filepath): + """Load translation from a file. + + Args: + filepath: Path to the translation file + + Returns: + translation: Loaded translation + """ + with open(filepath, "r", encoding="utf-8") as file: + translation = json.load(file) + return translation + + +def check_translations(): + """Load default translation and compare with other translations.""" + default_translation = load_translation("lang/en.json") + translations_dir = "lang" + translations = os.listdir(translations_dir) + translations.remove("en.json") # Exclude default translation + + files_with_missing_translations = [] + + for translation_file in translations: + translation_path = os.path.join(translations_dir, translation_file) + other_translation = load_translation(translation_path) + + # Compare translations + missing_translations = compare_translations( + default_translation, other_translation + ) + if missing_translations: + file_translation = FileTranslation(translation_file, missing_translations) + files_with_missing_translations.append(file_translation) + + for file_translation in files_with_missing_translations: + print( + f"File {translations_dir}/{file_translation.file} has missing translations for:" + ) + for key in file_translation.missing_translations: + print(f" - {key}") + + if files_with_missing_translations: + sys.exit(1) # Exit with an error status code + else: + print("All translations are present") + sys.exit(0) + + +if __name__ == "__main__": + check_translations() + # Exit with a success status code diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6762757ced..c6e5cd1143 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -16,8 +16,8 @@ on: - 'master' env: - CODECOV_UNIQUE_NAME: CODECOV_UNIQUE_NAME-${{ github.run_id }}-${{ github.run_number }} - + CODECOV_UNIQUE_NAME: CODECOV_UNIQUE_NAME-${{ github.run_id }}-${{ github.run_number }} + jobs: Flutter-Codebase-Check: name: Checking codebase @@ -27,11 +27,11 @@ jobs: - uses: actions/checkout@v3 with: # ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 + fetch-depth: 0 - uses: actions/setup-java@v3 with: distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '12.0' + java-version: '12.0' - uses: subosito/flutter-action@v2 with: flutter-version: '3.16.0' @@ -66,11 +66,16 @@ jobs: git checkout temp_branch pip install GitPython python ./.github/workflows/check_ignore.py --repository ${{github.repository}} --merge_branch_name ${{github.head_ref}} + - name: Compare translation files + run: | + chmod +x .github/workflows/compare_translations.py + python .github/workflows/compare_translations.py + - name: Analysing codebase for default linting run: flutter analyze --no-pub - name: Analysing codebase for custom linting run: flutter pub run custom_lint - - name : Changed Files + - name: Changed Files id: changed-files uses: tj-actions/changed-files@v35 - name: List all changed files @@ -104,7 +109,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '12.0' + java-version: '12.0' - uses: subosito/flutter-action@v2 with: flutter-version: '3.16.0' @@ -118,13 +123,13 @@ jobs: with: verbose: true fail_ci_if_error: false - name: '${{env.CODECOV_UNIQUE_NAME}}' + name: '${{env.CODECOV_UNIQUE_NAME}}' - name: Test acceptable level of code coverage uses: VeryGoodOpenSource/very_good_coverage@v2 with: - path: './coverage/lcov.info' - min_coverage: 88.0 - + path: './coverage/lcov.info' + min_coverage: 88.0 + Android-Build: name: Testing build for android runs-on: ubuntu-latest @@ -134,7 +139,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '12.0' + java-version: '12.0' - uses: subosito/flutter-action@v2 with: flutter-version: '3.16.0' diff --git a/pubspec.lock b/pubspec.lock index 6f89532a1b..b0b9bbc7e0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1013,6 +1013,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "41b90ceaec6d79819f31e975e61d479516efe701dea35f891b2f986c1b031422" + url: "https://pub.dev" + source: hosted + version: "9.0.17" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "54808cfcfa87dbc0d74c61ac063d624adf1bd5c0407301f32b06c783c60dc4ca" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "7e71be3c161472f6c9158ac8875dd8de575060d60b5d159ebca3600ea32c9116" + url: "https://pub.dev" + source: hosted + version: "1.0.6" lint: dependency: "direct dev" description: @@ -1033,26 +1057,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -1137,10 +1161,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1834,5 +1858,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <3.13.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <3.13.0" + flutter: ">=3.16.0" From c3134677b5d8d8fc44bdb7521aaf115dc009591f Mon Sep 17 00:00:00 2001 From: Manik Mehta Date: Fri, 29 Dec 2023 08:31:06 +0530 Subject: [PATCH 16/29] Pinned Post Page test written (#2280) --- .../feed/pinned_post_page_test.dart | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test/widget_tests/after_auth_screens/feed/pinned_post_page_test.dart b/test/widget_tests/after_auth_screens/feed/pinned_post_page_test.dart index e2ee03f619..4d0b3957a9 100644 --- a/test/widget_tests/after_auth_screens/feed/pinned_post_page_test.dart +++ b/test/widget_tests/after_auth_screens/feed/pinned_post_page_test.dart @@ -11,6 +11,9 @@ import 'package:talawa/router.dart' as router; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/views/after_auth_screens/feed/pinned_post_page.dart'; +import 'package:talawa/widgets/post_list_widget.dart'; +import 'package:talawa/widgets/post_widget.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import '../../../helpers/test_helpers.dart'; @@ -44,14 +47,16 @@ void main() { }); group('Tests for pinned post page', () { - // testWidgets('Check whether PinnedPostPage shows up', (tester) async { - // VisibilityDetectorController.instance.updateInterval = Duration.zero; - // - // await tester.pumpWidget(createPinnedPostPage()); - // await tester.pump(); - // - // expect(find.byType(PinnedPostPage), findsOneWidget); - // expect(find.byType(PostListWidget), findsOneWidget); - // }); + testWidgets('Check whether PinnedPostPage shows up', (tester) async { + VisibilityDetectorController.instance.updateInterval = Duration.zero; + + await tester.pumpWidget(createPinnedPostPage()); + await tester.pump(); + + expect(find.byType(PinnedPostPage), findsOneWidget); + expect(find.byType(PostListWidget), findsOneWidget); + expect(find.byType(NewsPost), findsOneWidget); + expect(find.textContaining('firstName1'), findsOneWidget); + }); }); } From c283b19ec46a0265468ff0dbdbb22762f9045e78 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:43:12 -0800 Subject: [PATCH 17/29] Update pull-request.yml --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c6e5cd1143..33826628d9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -128,7 +128,7 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v2 with: path: './coverage/lcov.info' - min_coverage: 88.0 + min_coverage: 90.0 Android-Build: name: Testing build for android From d45a4a4cb562005c91989b01d752e090a3660f6c Mon Sep 17 00:00:00 2001 From: ANKIT VARSHNEY <132201033+AVtheking@users.noreply.github.com> Date: Fri, 29 Dec 2023 17:49:34 +0530 Subject: [PATCH 18/29] Fix/actions for translation files (#2281) * added check for all translation file * fix falling test * fix the python style guide * made the requested changes * now the script will report for every file if there is error * revert back the double quotes to single quotes * error message will show path of file * improved the output message * used namedtuple to improve readability * used argparser * made the changes * made the changes --- .github/workflows/compare_translations.py | 56 ++++++++++++++++++++--- .github/workflows/pull-request.yml | 2 +- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.github/workflows/compare_translations.py b/.github/workflows/compare_translations.py index 1799240d95..384f25e28b 100644 --- a/.github/workflows/compare_translations.py +++ b/.github/workflows/compare_translations.py @@ -8,7 +8,9 @@ This module defines a function to compare two translations and print any missing keys in the other language's translation. Attributes: - FileTranslation (namedtuple): Named tuple to represent a combination of file and missing translations. + + FileTranslation : Named tuple to represent a combination of file and missing translations. + Fields: - file (str): The file name. - missing_translations (list): List of missing translations. @@ -17,9 +19,18 @@ compare_translations(default_translation, other_translation): Compare two translations and print missing keys. + load_translation(filepath): + Load translation from a file. + check_translations(): Load the default translation and compare it with other translations. + main(): + The main function to run the script. + Parses command-line arguments, checks for the existence of the specified directory, + and then calls check_translations with the provided or default directory. + + Usage: This script can be executed to check and print missing translations in other languages based on the default English translation. @@ -37,6 +48,7 @@ """ # standard imports +import argparse import json import os import sys @@ -79,10 +91,18 @@ def load_translation(filepath): return translation -def check_translations(): - """Load default translation and compare with other translations.""" + +def check_translations(directory): + """Load default translation and compare with other translations. + + Args: + directory (str): The directory containing translation files. + + Returns: + None + """ default_translation = load_translation("lang/en.json") - translations_dir = "lang" + translations_dir = directory translations = os.listdir(translations_dir) translations.remove("en.json") # Exclude default translation @@ -114,6 +134,30 @@ def check_translations(): sys.exit(0) +def main(): + """ + Parse command-line arguments, check for the existence of the specified directory, + and call check_translations with the provided or default directory. + + """ + parser = argparse.ArgumentParser( + description="Check and print missing translations for all non-default languages." + ) + parser.add_argument( + "--directory", + type=str, + nargs="?", + default=os.path.join(os.getcwd(), "lang"), + help="Directory containing translation files(relative to the root directory).", + ) + args = parser.parse_args() + + if not os.path.exists(args.directory): + print(f"Error: The specified directory '{args.directory}' does not exist.") + sys.exit(1) + + check_translations(args.directory) + + if __name__ == "__main__": - check_translations() - # Exit with a success status code + main() diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 33826628d9..f7e602e6d7 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -69,7 +69,7 @@ jobs: - name: Compare translation files run: | chmod +x .github/workflows/compare_translations.py - python .github/workflows/compare_translations.py + python .github/workflows/compare_translations.py --directory lang - name: Analysing codebase for default linting run: flutter analyze --no-pub From 23824b09ebda879b60bbbd7d21ed31dfc96108ce Mon Sep 17 00:00:00 2001 From: Parag Gupta <103507835+Dante291@users.noreply.github.com> Date: Sat, 30 Dec 2023 03:02:05 +0530 Subject: [PATCH 19/29] Script Update: Detailed Error Output for Translation Mismatches (#2284) * Verification of Translation Comparison Script Functionality * testing when default language has 1 extra key * testing when default language has 1 extra key * testing when key in DL has different spacing in a key * testing multiple extra keys in DL * testing when key is wrong in other translation files * Modifying script to give more detailed description for the missing or mismatched key * testing * testing * testing * testing * more detailed description for error * testing * testing if new script displays the correct error for new key that is not present in non default files * testing for wrong key in non default * modified compare translation script * testing * testing * modified --- .github/workflows/compare_translations.py | 71 ++++++++++++----------- lang/de.json | 1 - lang/en.json | 7 --- lang/es.json | 2 - lang/fr.json | 4 -- lang/ja.json | 3 - lang/pt.json | 2 - lang/zh.json | 1 - 8 files changed, 38 insertions(+), 53 deletions(-) diff --git a/.github/workflows/compare_translations.py b/.github/workflows/compare_translations.py index 384f25e28b..d8c0aff797 100644 --- a/.github/workflows/compare_translations.py +++ b/.github/workflows/compare_translations.py @@ -58,23 +58,35 @@ FileTranslation = namedtuple("FileTranslation", ["file", "missing_translations"]) -def compare_translations(default_translation, other_translation): - """Compare two translations and print missing keys. +def compare_translations(default_translation, other_translation, default_file, other_file): + """Compare two translations and return detailed info about missing/mismatched keys. Args: - default_translation: The default translation - other_translation: The other translation + default_translation (dict): The default translation (en.json). + other_translation (dict): The other language translation. + default_file (str): The name of the default translation file. + other_file (str): The name of the other translation file. Returns: - missing_translations: List of missing translations + list: A list of detailed error messages for each missing/mismatched key. """ - missing_translations = [] + errors = [] + # Check for missing keys in other_translation for key in default_translation: if key not in other_translation: - missing_translations.append(key) + error_msg = f"Missing Key: '{key}' - This key from '{default_file}' is missing in '{other_file}'." + errors.append(error_msg) + + # Check for keys in other_translation that don't match any in default_translation + for key in other_translation: + if key not in default_translation: + error_msg = f"Error Key: '{key}' - This key in '{other_file}' does not match any key in '{default_file}'." + errors.append(error_msg) + + return errors + - return missing_translations def load_translation(filepath): @@ -95,39 +107,32 @@ def load_translation(filepath): def check_translations(directory): """Load default translation and compare with other translations. - Args: + Args: directory (str): The directory containing translation files. Returns: None """ - default_translation = load_translation("lang/en.json") - translations_dir = directory - translations = os.listdir(translations_dir) - translations.remove("en.json") # Exclude default translation + default_file = "en.json" + default_translation = load_translation(os.path.join(directory, default_file)) + translations = os.listdir(directory) + translations.remove(default_file) # Exclude default translation - files_with_missing_translations = [] + error_found = False for translation_file in translations: - translation_path = os.path.join(translations_dir, translation_file) - other_translation = load_translation(translation_path) - - # Compare translations - missing_translations = compare_translations( - default_translation, other_translation - ) - if missing_translations: - file_translation = FileTranslation(translation_file, missing_translations) - files_with_missing_translations.append(file_translation) - - for file_translation in files_with_missing_translations: - print( - f"File {translations_dir}/{file_translation.file} has missing translations for:" - ) - for key in file_translation.missing_translations: - print(f" - {key}") - - if files_with_missing_translations: + other_file = os.path.join(directory, translation_file) + other_translation = load_translation(other_file) + + # Compare translations and get detailed error messages + errors = compare_translations(default_translation, other_translation, default_file, translation_file) + if errors: + error_found = True + print(f"File {translation_file} has missing translations for:") + for error in errors: + print(f" - {error}") + + if error_found: sys.exit(1) # Exit with an error status code else: print("All translations are present") diff --git a/lang/de.json b/lang/de.json index d903796ca1..740a2fb83d 100644 --- a/lang/de.json +++ b/lang/de.json @@ -152,7 +152,6 @@ "Logout": "Ausloggen", "Settings": "Einstellungen", "Dark Theme": "Dunkles Thema", - "Error": "Fout", "Warning": "Waarschuwing", "Information": "Informatie", diff --git a/lang/en.json b/lang/en.json index 09bdd9211f..c7cf4c0b77 100644 --- a/lang/en.json +++ b/lang/en.json @@ -12,7 +12,6 @@ "Enter your password": "Enter your password", "Forgot password": "Forgot password", "Login": "Login", - "Notification Feature is not installed": "Notification Feature is not installed", "Sit back relax, we'll": "Sit back relax, we'll", "Recover": "Recover", @@ -35,7 +34,6 @@ "Next": "Next", "Request Sent to": "Request Sent to", "Log out": "Log out", - "Error": "Error", "Warning": "Warning", "Information": "Information", @@ -46,7 +44,6 @@ "Collaborate": "Collaborate", "with your": "with your", "Organizations": "Organizations", - "Title from the viewMode GSoC branch": "Title from the viewMode GSoC branch", "Please verify URL first": "Please verify URL first", "Enter a valid URL": "Enter a valid URL", @@ -61,20 +58,16 @@ "Password must not contain spaces": "Password must not contain spaces", "Password does not match original": "Password does not match original", "Join Organisation": "Join Organisation", - "We're": "We're", "Glad": "Glad", "you're": "you're", "Back": "Back", - "Let's": "Let's", "get": "get", "you": "you", "SignUp": "SignUp", - "Please wait": "Please wait", "for organisation(s) to accept your invitation.": "for organisation(s) to accept your invitation.", - "Add Event Title": "Add Event Title", "Where is the event": "Where is the event", "Add Location": "Add Location", diff --git a/lang/es.json b/lang/es.json index d1060a09b5..9401739d42 100644 --- a/lang/es.json +++ b/lang/es.json @@ -14,9 +14,7 @@ "Login": "Acceso", "Sit back relax, we'll": "Siéntese, ", "Recover": "", - "Notification Feature is not installed": "La función de notificación no está instalada", - "your password": "relájese, recuperaremos su contraseña", "Recover Password": "Recupera tu contraseña", "Select Language": "Seleccione el idioma", diff --git a/lang/fr.json b/lang/fr.json index 6db60cd069..8bea3b769d 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -7,10 +7,8 @@ "Enter new password": "Entrez un nouveau mot de passe", "Re-Enter your password": "Entrez à nouveau votre mot de passe", "Change Password": "Changer le mot de passe", - "Email Hint": "test@test.org", "Notification Feature is not installed": "La fonction de notification n'est pas installée", - "Enter your registered Email": "Entrez votre email enregistré", "Enter your password": "Tapez votre mot de passe", "Forgot password": "Mot de passe oublié", @@ -36,7 +34,6 @@ "Next": "Suivante", "Request Sent to": "Demande envoyée à", "Log out": "Se déconnecter", - "Join": "Rejoignez", "and": "et", "Collaborate": "collaborez", @@ -61,7 +58,6 @@ "Password must not contain spaces": "Le mot de passe ne doit pas contenir d'espaces", "Password does not match original": "Le mot de passe ne correspond pas à l'original", "Join Organisation": "Rejoindre l'organisation", - "We're": "Nous sommes", "Glad": "heureux que vous", "you're": "soyez de", diff --git a/lang/ja.json b/lang/ja.json index 98b00fe3f8..22953de4f8 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -14,7 +14,6 @@ "Login": "ログイン", "Sit back relax, we'll": "ゆったりとおくつろぎください。", "Recover": "回復", - "Notification Feature is not installed": "通知機能がインストールされていません", "your password": "あなたのパスワード", "Recover Password": "パスワードを回復", @@ -62,10 +61,8 @@ "get": "得る", "you": "あなた", "SignUp": "サインアップ", - "Please wait": "お待ちください", "for organisation(s) to accept your invitation.": "組織があなたの招待を受け入れるために。", - "Add Event Title": "イベントタイトルを追加", "Where is the event": "イベントはどこですか", "Add Location": "場所を追加", diff --git a/lang/pt.json b/lang/pt.json index 1bd61aad6b..1dafd483fa 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -13,9 +13,7 @@ "Forgot password": "Esqueceu sua senha", "Login": "Conecte-se", "Sit back relax, we'll": "Sente-se relaxe, vamos", - "Notification Feature is not installed": "O recurso de notificação não está instalado", - "Recover": "Recuperar", "your password": "sua senha", "Recover Password": "Recuperar senha", diff --git a/lang/zh.json b/lang/zh.json index ef0a204878..3f587f4cfe 100644 --- a/lang/zh.json +++ b/lang/zh.json @@ -15,7 +15,6 @@ "Sit back relax, we'll": "高枕无忧, ", "Recover": "", "Notification Feature is not installed": "未安装通知功能\n", - "your password": "我们会找回您的密码", "Recover Password": "恢复你的密码", "Select Language": "选择语言", From 858c63301845fc0baeec2990444b6c400e6e6029 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sat, 30 Dec 2023 10:03:40 -0800 Subject: [PATCH 20/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e8d4e8f0c..c50e83c15a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talawa -[💬 Join the community on Slack](https://join.slack.com/t/thepalisadoes-dyb6419/shared_invite/zt-28cswzsds-gfw~qPoxZOZv3vOYKokDKw) +[💬 Join the community on Slack](https://join.slack.com/t/thepalisadoes-dyb6419/shared_invite/zt-29oltereu-qJ931LcKxswuCAy29iA9WA) ![talawa-logo-lite-200x200](https://github.com/PalisadoesFoundation/talawa-admin/assets/16875803/26291ec5-d3c1-4135-8bc7-80885dff613d) From 97c1ee73cfb1eb888247603afe5b9892dffd6051 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Sun, 31 Dec 2023 02:21:49 +0530 Subject: [PATCH 21/29] Database mutations test added (#2285) * Updated Code * Updated Code --- lib/services/database_mutation_functions.dart | 4 +- test/helpers/test_helpers.dart | 15 +- .../database_mutations_function_test.dart | 1079 +++++++++++++++++ 3 files changed, 1089 insertions(+), 9 deletions(-) create mode 100644 test/service_tests/database_mutations_function_test.dart diff --git a/lib/services/database_mutation_functions.dart b/lib/services/database_mutation_functions.dart index f026498c51..03d6b1d95a 100644 --- a/lib/services/database_mutation_functions.dart +++ b/lib/services/database_mutation_functions.dart @@ -393,10 +393,10 @@ class DataBaseMutationFunctions { fetchOrgById(id); } } else if (result.data != null && result.isConcrete) { + print(result.data!['organizations']); return OrgInfo.fromJson( // ignore: collection_methods_unrelated_type - (result.data!['organizations'] as Map)[0] - as Map, + (result.data!['organizations'] as List>)[0], ); } return false; diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 392d273c44..71ebf67576 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -236,13 +236,14 @@ GraphqlConfig getAndRegisterGraphqlConfig() { }); when(service.authClient()).thenAnswer((realInvocation) { - final AuthLink authLink = - AuthLink(getToken: () async => 'Bearer ${GraphqlConfig.token}'); - final Link finalAuthLink = authLink.concat(service.httpLink); - return GraphQLClient( - cache: GraphQLCache(partialDataPolicy: PartialDataCachePolicy.accept), - link: finalAuthLink, - ); + // final AuthLink authLink = + // AuthLink(getToken: () async => 'Bearer ${GraphqlConfig.token}'); + // final Link finalAuthLink = authLink.concat(service.httpLink); + // return GraphQLClient( + // cache: GraphQLCache(partialDataPolicy: PartialDataCachePolicy.accept), + // link: finalAuthLink, + // ); + return locator(); }); when(service.getToken()).thenAnswer((_) async => "sample_token"); diff --git a/test/service_tests/database_mutations_function_test.dart b/test/service_tests/database_mutations_function_test.dart new file mode 100644 index 0000000000..75b821c2c6 --- /dev/null +++ b/test/service_tests/database_mutations_function_test.dart @@ -0,0 +1,1079 @@ +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:hive/hive.dart'; +import 'package:mockito/mockito.dart'; +import 'package:talawa/enums/enums.dart'; +import 'package:talawa/models/organization/org_info.dart'; +import 'package:talawa/services/database_mutation_functions.dart'; +import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/utils/queries.dart'; +import '../helpers/test_helpers.dart'; +import '../helpers/test_locator.dart'; + +/// Tests database_mutations_functions.dart. +/// +/// more_info_if_required +/// +/// **params**: +/// None +/// +/// **returns**: +/// None +void main() async { + testSetupLocator(); + locator().test(); + late DataBaseMutationFunctions functionsClass; + final Directory dir = await Directory.systemTemp.createTemp('talawa_test'); + Hive.init(dir.path); + await Hive.openBox('url'); + + const userNotAuthenticated = + GraphQLError(message: 'User is not authenticated'); + + const userNotAuthenticatedrand = + GraphQLError(message: 'User is not authenticatedrand'); + const userNotFound = GraphQLError(message: 'User not found'); + const refreshAccessTokenExpiredException = GraphQLError( + message: + 'Access Token has expired. Please refresh session.: Undefined location', + ); + + const wrongCredentials = GraphQLError(message: 'Invalid credentials'); + + const organizationNotFound = GraphQLError(message: 'Organization not found'); + + const memberRequestExist = + GraphQLError(message: 'Membership Request already exists'); + + const notifFeatureNotInstalled = GraphQLError( + message: + 'Failed to determine project ID: Error while making request: getaddrinfo ENOTFOUND metadata.google.internal. Error code: ENOTFOUND', + ); + + const emailAccountPresent = + GraphQLError(message: 'Email address already exists'); + + final testOrg = OrgInfo.fromJson({ + 'image': 'sampleimg', + 'id': 'XYZ', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }); + + setUpAll(() { + registerServices(); + functionsClass = DataBaseMutationFunctions(); + functionsClass.init(); + }); + + group('Database Mutation Functions Tests', () { + testWidgets('Widget Testing 1', (tester) async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException( + graphqlErrors: [userNotAuthenticated], + linkException: UnknownException( + userNotAuthenticated, + StackTrace.current, + ), + ), + source: QueryResultSource.network, + ), + ); + + await functionsClass.gqlNonAuthQuery(query); + + tester.binding.addPostFrameCallback((_) { + navigationService.showTalawaErrorSnackBar( + "Server not running/wrong url", + MessageType.error, + ); + }); + + await tester.pump(); + }); + + testWidgets('Widget Testing 2', (tester) async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException( + graphqlErrors: [userNotAuthenticatedrand], + ), + source: QueryResultSource.network, + ), + ); + + await functionsClass.gqlNonAuthQuery(query); + + tester.binding.addPostFrameCallback((_) { + navigationService.showTalawaErrorSnackBar( + "Something went wrong!", + MessageType.error, + ); + }); + + await tester.pump(); + }); + + test('fetchOrgById test in case of successful results', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions( + document: gql(queries.fetchOrgById('XYZ')), + ), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + data: { + 'organizations': [ + { + 'id': 'XYZ', + 'image': 'sampleimg', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }, + ], + }, + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ') as OrgInfo; + + expect(org.id, testOrg.id); + expect(org.name, testOrg.name); + expect(org.image, testOrg.image); + expect(org.isPublic, testOrg.isPublic); + expect(org.creatorInfo!.firstName, testOrg.creatorInfo!.firstName); + }); + + test('fetchOrgById test in case of exception', () async { + final String query = Queries().fetchOrgById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when( + locator().mutate( + MutationOptions(document: gql(query)), + ), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.fetchOrgById('XYZ'); + expect(res, false); + }); + + test('Testing wrong credential error', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions( + document: gql( + queries.fetchOrgById('XYZ'), + ), + ), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + exception: OperationException(graphqlErrors: [wrongCredentials]), + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ'); + expect(org, false); + }); + + test('Testing organization not found error', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions( + document: gql( + queries.fetchOrgById('XYZ'), + ), + ), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + exception: OperationException(graphqlErrors: [organizationNotFound]), + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ'); + expect(org, false); + }); + + test('Testing memberRequestExist error', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions( + document: gql(queries.fetchOrgById('XYZ')), + ), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + exception: OperationException(graphqlErrors: [memberRequestExist]), + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ'); + expect(org, false); + }); + + test('Testing emailAccountPresent error', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions(document: gql(queries.fetchOrgById('XYZ'))), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + exception: OperationException( + graphqlErrors: [emailAccountPresent], + ), + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ'); + expect(org, false); + }); + + test('Testing notifFeatureNotInstalled error', () async { + when(locator().clientToQuery()).thenAnswer( + (_) => locator(), + ); + + when( + locator().mutate( + MutationOptions(document: gql(queries.fetchOrgById('XYZ'))), + ), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(queries.fetchOrgById('XYZ'))), + exception: OperationException( + graphqlErrors: [notifFeatureNotInstalled], + ), + source: QueryResultSource.network, + ), + ); + + final org = await functionsClass.fetchOrgById('XYZ'); + expect(org, false); + }); + + test('Testing gqlAuthQuery function without exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + data: { + 'organizations': [ + { + 'id': 'XYZ', + 'image': 'sampleimg', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }, + ], + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthQuery(query) as QueryResult; + final org = OrgInfo.fromJson( + (res.data!['organizations'] as List>)[0], + ); + + expect(org.id, testOrg.id); + expect(org.name, testOrg.name); + expect(org.image, testOrg.image); + expect(org.isPublic, testOrg.isPublic); + expect(org.creatorInfo!.firstName, testOrg.creatorInfo!.firstName); + }); + + test('Testing gqlAuthQuery with false exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException(graphqlErrors: [userNotFound]), + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthQuery(query); + expect(res, null); + }); + + test('Testing gqlAuthQuery with true exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthQuery(query); + expect(res, null); + }); + + test('Test for gql auth mutation', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + data: { + 'organizations': [ + { + 'id': 'XYZ', + 'image': 'sampleimg', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }, + ], + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthMutation(query) as QueryResult; + final org = OrgInfo.fromJson( + (res.data!['organizations'] as List>)[0], + ); + + expect(org.id, testOrg.id); + expect(org.name, testOrg.name); + expect(org.image, testOrg.image); + expect(org.isPublic, testOrg.isPublic); + expect(org.creatorInfo!.firstName, testOrg.creatorInfo!.firstName); + }); + + test('Test for gql auth mutation with false exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException(graphqlErrors: [userNotFound]), + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthMutation(query); + expect(res, null); + }); + + test('Test for gql auth mutation with true exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlAuthMutation(query); + expect(res, null); + }); + + test('Test for gql non auth query', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + data: { + 'organizations': [ + { + 'id': 'XYZ', + 'image': 'sampleimg', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }, + ], + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthQuery(query); + final org = OrgInfo.fromJson( + (res!.data!['organizations'] as List>)[0], + ); + + expect(org.id, testOrg.id); + expect(org.name, testOrg.name); + expect(org.image, testOrg.image); + expect(org.isPublic, testOrg.isPublic); + expect(org.creatorInfo!.firstName, testOrg.creatorInfo!.firstName); + }); + + test('Test for gql non auth mutation', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + data: { + 'organizations': [ + { + 'id': 'XYZ', + 'image': 'sampleimg', + 'name': 'Sample1', + 'isPublic': true, + 'creator': {'firstName': 'Shivam', 'lastName': 'Gupta'}, + }, + ], + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthMutation(query) as QueryResult; + final org = OrgInfo.fromJson( + (res.data!['organizations'] as List>)[0], + ); + + expect(org.id, testOrg.id); + expect(org.name, testOrg.name); + expect(org.image, testOrg.image); + expect(org.isPublic, testOrg.isPublic); + expect(org.creatorInfo!.firstName, testOrg.creatorInfo!.firstName); + }); + + test('Test for gql non auth mutation with false exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException(graphqlErrors: [userNotFound]), + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthMutation(query); + expect(res, null); + }); + + test('Test for gql non auth mutation with true exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthMutation(query); + expect(res, null); + }); + + test('Test for refresh access token', () async { + final String query = Queries().refreshToken('reftok123'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + data: { + 'refreshToken': { + 'accessToken': 'acctok123', + 'refreshToken': 'reftok123', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.refreshAccessToken('reftok123'); + verify( + userConfig.updateAccessToken( + accessToken: 'acctok123', + refreshToken: 'reftok123', + ), + ); + verify(databaseFunctions.init()); + expect(res, true); + }); + + test('Test for refresh access token with false exception', () async { + final String query = Queries().refreshToken('reftok123'); + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException(graphqlErrors: [userNotFound]), + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.refreshAccessToken('reftok123'); + verify(navigationService.pop()); + expect(res, false); + }); + + test('Test for refresh access token with true exception', () async { + final String query = Queries().refreshToken('x'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when( + locator().mutate(MutationOptions(document: gql(query))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: MutationOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.refreshAccessToken('x'); + expect(res, false); + }); + + test('Test for gql non auth query with link exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: OperationException( + graphqlErrors: [userNotFound], + linkException: UnknownException( + userNotFound, + StackTrace.current, + ), + ), + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthQuery(query); + expect(res, null); + }); + + test('Test for gql non auth query with true exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [refreshAccessTokenExpiredException], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthQuery(query); + expect(res, null); + }); + test('Test for gql non auth query with false exception', () async { + final String query = Queries().fetchOrgDetailsById('XYZ'); + final String query2 = Queries().refreshToken('abc'); + final String query3 = Queries().refreshToken('xyz'); + + userConfig.currentUser.refreshToken = 'abc'; + + /// Returns the exception to be thrown in case of refresh token expired + /// + /// more_info_if_required + /// + /// **params**: + /// None + /// + /// **returns**: + /// * `Map`: Map of exception to be thrown + Map exp2() { + if (userConfig.currentUser.refreshToken == 'abc') { + userConfig.currentUser.refreshToken = 'xyz'; + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotAuthenticated], + ), + }); + } else { + return Map.from({ + 'val': OperationException( + graphqlErrors: [userNotFound], + ), + }); + } + } + + when(locator().query(QueryOptions(document: gql(query)))) + .thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query)), + exception: exp2()['val'], + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query2))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query2)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + when( + locator().mutate(MutationOptions(document: gql(query3))), + ).thenAnswer( + (_) async => QueryResult( + options: QueryOptions(document: gql(query3)), + data: { + 'refreshToken': { + 'accessToken': 'testtoken', + 'refreshToken': 'testtoken', + }, + }, + source: QueryResultSource.network, + ), + ); + + final res = await functionsClass.gqlNonAuthQuery(query); + expect(res, null); + }); + }); +} From eb5ed77ded6ecd65bd47087724098e7c6714ce41 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sat, 30 Dec 2023 14:07:40 -0800 Subject: [PATCH 22/29] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5696cb8a4..23a807c667 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,6 +100,7 @@ The process of proposing a change to Talawa can be summarized as: 1. Pull requests that don't meet the minimum test coverage levels will not be accepted. This may mean that you will have to create tests for code you did not write. You can decide which part of the code base needs additional tests if this happens to you. 1. **_Testing_:** 1. Test using the `flutter test` command. + 1. Review [Flutter's official introduction to unit testing](https://docs.flutter.dev/cookbook/testing/unit/introduction) 1. Here are some useful flutter test videos 1. [State Management With Provider](https://www.raywenderlich.com/6373413-state-management-with-provider) 1. [Unit Testing With Flutter: Getting Started](https://www.raywenderlich.com/6926998-unit-testing-with-flutter-getting-started) From d8b9a043b411354d1e59c70aea3b0dbf2e2e55f2 Mon Sep 17 00:00:00 2001 From: ANKIT VARSHNEY <132201033+AVtheking@users.noreply.github.com> Date: Sun, 31 Dec 2023 22:57:35 +0530 Subject: [PATCH 23/29] test for access request screen (#2286) * test for access request screen * fixed falling test cases * fix falling test * fix falling test --- test/helpers/test_locator.dart | 2 + .../access_request_screen_test.dart | 61 +++++++++++++++++++ .../join_organisation_after_auth_test.dart | 6 +- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 test/views/after_auth_screens/join_org_after_auth_test/access_request_screen_test.dart rename test/views/after_auth_screens/{ => join_org_after_auth_test}/join_organisation_after_auth_test.dart (98%) diff --git a/test/helpers/test_locator.dart b/test/helpers/test_locator.dart index f11dcb1d90..f3a58d3bb1 100644 --- a/test/helpers/test_locator.dart +++ b/test/helpers/test_locator.dart @@ -17,6 +17,7 @@ import 'package:talawa/services/size_config.dart'; import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; import 'package:talawa/services/user_config.dart'; import 'package:talawa/utils/queries.dart'; +import 'package:talawa/view_model/access_request_view_model.dart'; import 'package:talawa/view_model/after_auth_view_models/add_post_view_models/add_post_view_model.dart'; import 'package:talawa/view_model/after_auth_view_models/chat_view_models/direct_chat_view_model.dart'; import 'package:talawa/view_model/after_auth_view_models/chat_view_models/select_contact_view_model.dart'; @@ -98,6 +99,7 @@ void testSetupLocator() { locator.registerFactory(() => LoginViewModel()); locator.registerFactory(() => SelectOrganizationViewModel()); + locator.registerFactory(() => AccessScreenViewModel()); locator.registerFactory(() => SignupDetailsViewModel()); locator.registerFactory(() => WaitingViewModel()); locator.registerFactory(() => ExploreEventsViewModel()); diff --git a/test/views/after_auth_screens/join_org_after_auth_test/access_request_screen_test.dart b/test/views/after_auth_screens/join_org_after_auth_test/access_request_screen_test.dart new file mode 100644 index 0000000000..f061229e0e --- /dev/null +++ b/test/views/after_auth_screens/join_org_after_auth_test/access_request_screen_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:talawa/utils/app_localization.dart'; +import 'package:talawa/view_model/access_request_view_model.dart'; +import 'package:talawa/views/after_auth_screens/join_org_after_auth/access_request_screen.dart'; +import 'package:talawa/views/base_view.dart'; + +import '../../../helpers/test_helpers.dart'; +import '../../../helpers/test_locator.dart'; + +Widget accessRequestScreen() { + return BaseView( + onModelReady: (model) => model.initialise(fakeOrgInfo), + builder: (context, model, child) { + return MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + AppLocalizationsDelegate(isTest: true), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + home: SendAccessRequest(org: fakeOrgInfo), + navigatorKey: navigationService.navigatorKey, + ); + }, + ); +} + +void main() { + testSetupLocator(); + setUp(() => registerServices()); + tearDown(() => unregisterServices()); + group("SendRequestAccess Screen test", () { + testWidgets("SendRequestAccess screen is build correctly", + (WidgetTester tester) async { + await tester.pumpWidget(accessRequestScreen()); + await tester.pumpAndSettle(); + + //Verify that appbar is present with transparent background color + expect(find.byType(AppBar), findsOneWidget); + final AppBar appBar = tester.firstWidget(find.byType(AppBar)); + expect(appBar.backgroundColor, Colors.transparent); + + //Verify that the image is present + expect(find.byType(Image), findsOneWidget); + expect(find.text("You need access"), findsOneWidget); + expect( + find.text("Request access, or switch to an account with access"), + findsOneWidget, + ); + expect(find.byType(TextField), findsOneWidget); + //Verify that the send request button is present + expect(find.text("Request Access"), findsOneWidget); + + //Tap the "Request Access" button and trigger a frame + await tester.tap(find.text("Request Access")); + await tester.pump(); + }); + }); +} diff --git a/test/views/after_auth_screens/join_organisation_after_auth_test.dart b/test/views/after_auth_screens/join_org_after_auth_test/join_organisation_after_auth_test.dart similarity index 98% rename from test/views/after_auth_screens/join_organisation_after_auth_test.dart rename to test/views/after_auth_screens/join_org_after_auth_test/join_organisation_after_auth_test.dart index 1fdcef83b8..aeb9075374 100644 --- a/test/views/after_auth_screens/join_organisation_after_auth_test.dart +++ b/test/views/after_auth_screens/join_org_after_auth_test/join_organisation_after_auth_test.dart @@ -17,9 +17,9 @@ import 'package:talawa/views/after_auth_screens/join_org_after_auth/join_organis import 'package:talawa/views/base_view.dart'; import 'package:talawa/widgets/organization_search_list.dart'; -import '../../helpers/test_helpers.dart'; -import '../../helpers/test_helpers.mocks.dart'; -import '../../helpers/test_locator.dart'; +import '../../../helpers/test_helpers.dart'; +import '../../../helpers/test_helpers.mocks.dart'; +import '../../../helpers/test_locator.dart'; Widget createJoinOrgAfterAuth({ String orgId = "fake_id", From fb34f9f6d75dda8401d66ea4a0903192ea49f329 Mon Sep 17 00:00:00 2001 From: Priyadharshini Nagarathinam <110827815+PriyadharshiniNagarathinam@users.noreply.github.com> Date: Sun, 31 Dec 2023 22:59:40 +0530 Subject: [PATCH 24/29] Event service test (#2288) * modify fetchRegistrantsByEvent test * test for dispose method * test for getters * fix format issues * fix format issues * fix codebase issues * add test for getEvents --- test/service_tests/event_service_test.dart | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/service_tests/event_service_test.dart b/test/service_tests/event_service_test.dart index 5c31e587e3..12970710d0 100644 --- a/test/service_tests/event_service_test.dart +++ b/test/service_tests/event_service_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:mockito/mockito.dart'; +import 'package:talawa/models/events/event_model.dart'; import 'package:talawa/services/database_mutation_functions.dart'; import 'package:talawa/services/event_service.dart'; import 'package:talawa/utils/event_queries.dart'; @@ -103,7 +104,7 @@ void main() { final dataBaseMutationFunctions = locator(); final query = TaskQueries.eventTasks('eventId'); when( - dataBaseMutationFunctions.gqlAuthMutation( + dataBaseMutationFunctions.gqlAuthQuery( EventQueries().registrantsByEvent('eventId'), ), ).thenAnswer( @@ -115,8 +116,8 @@ void main() { source: QueryResultSource.network, ), ); - final services = EventQueries(); - services.registrantsByEvent('eventId'); + final services = EventService(); + services.fetchRegistrantsByEvent('eventId'); }); test('Test getEvents method', () async { @@ -139,8 +140,18 @@ void main() { source: QueryResultSource.network, ), ); - final services = EventQueries(); - services.fetchOrgEvents('OrgId'); + final services = EventService(); + services.getEvents(); + }); + + test('Test dispose method', () { + final eventService = EventService(); + eventService.dispose(); + }); + + test('Test for getters', () { + final model = EventService(); + expect(model.eventStream, isA>()); }); }); } From 2ed517b9d1d70cb1104b1977bdf45bc4c980b675 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 19:41:26 -0800 Subject: [PATCH 25/29] Bump cached_network_image from 3.3.0 to 3.3.1 (#2295) Bumps [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) from 3.3.0 to 3.3.1. - [Commits](https://github.com/Baseflow/flutter_cached_network_image/compare/v3.3.0...v3.3.1) --- updated-dependencies: - dependency-name: cached_network_image dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 52 ++++++++++++++-------------------------------------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b0b9bbc7e0..ebb70cf970 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,26 +141,26 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" characters: dependency: transitive description: @@ -1013,30 +1013,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "41b90ceaec6d79819f31e975e61d479516efe701dea35f891b2f986c1b031422" - url: "https://pub.dev" - source: hosted - version: "9.0.17" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "54808cfcfa87dbc0d74c61ac063d624adf1bd5c0407301f32b06c783c60dc4ca" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "7e71be3c161472f6c9158ac8875dd8de575060d60b5d159ebca3600ea32c9116" - url: "https://pub.dev" - source: hosted - version: "1.0.6" lint: dependency: "direct dev" description: @@ -1057,26 +1033,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" mime: dependency: transitive description: @@ -1161,10 +1137,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_parsing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b158cf4ef0..811d933076 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: ################################ auto_size_text: ^3.0.0 - cached_network_image: ^3.1.0 + cached_network_image: ^3.3.1 connectivity_plus: ^5.0.2 contained_tab_bar_view: ^0.8.0 From 05bc12c373775fb1a1701e89f72eb6eb6563dba9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 19:42:15 -0800 Subject: [PATCH 26/29] Bump syncfusion_flutter_datepicker from 24.1.41 to 24.1.43 (#2294) Bumps [syncfusion_flutter_datepicker](https://github.com/syncfusion/flutter-widgets/tree/master/packages) from 24.1.41 to 24.1.43. - [Release notes](https://github.com/syncfusion/flutter-widgets/releases) - [Commits](https://github.com/syncfusion/flutter-widgets/commits/HEAD/packages) --- updated-dependencies: - dependency-name: syncfusion_flutter_datepicker dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ebb70cf970..59861d3f31 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1558,18 +1558,18 @@ packages: dependency: transitive description: name: syncfusion_flutter_core - sha256: "69c827931957d5b121ee9f0b9b0b8d7d0d1ac537b61bcdd5c3fbffc044bbe86e" + sha256: "1b40729aa10a727150a6cc56e532c770f4baded83846fca8700efd908d0f4d0a" url: "https://pub.dev" source: hosted - version: "24.1.41" + version: "24.1.43" syncfusion_flutter_datepicker: dependency: "direct main" description: name: syncfusion_flutter_datepicker - sha256: "13c48582dc911663eac286791f009474fae964a731dc719d86ba0de8d8ade6f0" + sha256: "3f9a8e8b585dd992d2321c899ec8d914634fb99eba02cf7a91c48a098bf62820" url: "https://pub.dev" source: hosted - version: "24.1.41" + version: "24.1.43" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 811d933076..e77204d659 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,7 +74,7 @@ dependencies: shimmer: ^3.0.0 social_share: ^2.2.1 syncfusion_flutter_calendar: ^24.1.41 - syncfusion_flutter_datepicker: any + syncfusion_flutter_datepicker: 24.1.43 timelines: ^0.1.0 tutorial_coach_mark: ^1.2.11 uni_links: ^0.5.1 From e9a296f3a78f560976e2e908f8a9c4fa32d623e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 19:43:08 -0800 Subject: [PATCH 27/29] Bump flutter_local_notifications from 16.2.0 to 16.3.0 (#2292) Bumps [flutter_local_notifications](https://github.com/MaikuB/flutter_local_notifications) from 16.2.0 to 16.3.0. - [Release notes](https://github.com/MaikuB/flutter_local_notifications/releases) - [Commits](https://github.com/MaikuB/flutter_local_notifications/compare/flutter_local_notifications-v16.2.0...flutter_local_notifications-v16.3.0) --- updated-dependencies: - dependency-name: flutter_local_notifications dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 59861d3f31..ca830aab27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -498,10 +498,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e + sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535" url: "https://pub.dev" source: hosted - version: "16.2.0" + version: "16.3.0" flutter_local_notifications_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e77204d659..88266f59e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: sdk: flutter flutter_braintree: ^3.0.0 flutter_cache_manager: ^3.3.1 - flutter_local_notifications: ^16.2.0 + flutter_local_notifications: ^16.3.0 flutter_localizations: sdk: flutter flutter_reaction_button: ^3.0.0+3 From 9991fbe622e49e5cc7040c01ea5bf543a19f266f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 20:07:03 -0800 Subject: [PATCH 28/29] Bump syncfusion_flutter_calendar from 24.1.41 to 24.1.43 (#2293) Bumps [syncfusion_flutter_calendar](https://github.com/syncfusion/flutter-widgets/tree/master/packages) from 24.1.41 to 24.1.43. - [Release notes](https://github.com/syncfusion/flutter-widgets/releases) - [Commits](https://github.com/syncfusion/flutter-widgets/commits/HEAD/packages) --- updated-dependencies: - dependency-name: syncfusion_flutter_calendar dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ca830aab27..d8d0d720c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1550,10 +1550,10 @@ packages: dependency: "direct main" description: name: syncfusion_flutter_calendar - sha256: ef1df99b0baf57c4b339d905a447633a8cbc612ac0e691ca0886591c9845fbb6 + sha256: b31182b348742b0f2285849179490afd89e321514b8e0eadeab6d7377373c9f2 url: "https://pub.dev" source: hosted - version: "24.1.41" + version: "24.1.43" syncfusion_flutter_core: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 88266f59e9..ffc2396a95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: shared_preferences: ^2.2.2 shimmer: ^3.0.0 social_share: ^2.2.1 - syncfusion_flutter_calendar: ^24.1.41 + syncfusion_flutter_calendar: ^24.1.43 syncfusion_flutter_datepicker: 24.1.43 timelines: ^0.1.0 tutorial_coach_mark: ^1.2.11 From 5e3e70ecce055bcf32e9e28e48be80336a4aff83 Mon Sep 17 00:00:00 2001 From: Ayush Raghuwanshi <62144720+AyushRaghuvanshi@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:25:37 +0530 Subject: [PATCH 29/29] Fixes #2192 #1602 , Improving Filters UI (#2245) * Add:Filters * Add:Documentation * Add:MultiLingual Support * Fix:Format * Reduced: Lines of Codes * Fixed:Tests * Add:Fix Failing test case * Add:Tests for requested lines * Add:test for filter changing * Fixed:changes requested * Add:Changes * Fix:Documentation * Fix:Dart version * Fixed:spacing * Remove: useless docs * Remove:Spaces * Removed:spaces * Fix:Constant Sizes * Fix:Format issues * Minor FIx * Fix:format * Fix:Dynamic size * Fix:Dynamic size * Fix:Failing test * Fix:Failing test --- lang/de.json | 10 ++ lang/en.json | 9 ++ lang/es.json | 9 ++ lang/fr.json | 9 ++ lang/hi.json | 9 ++ lang/ja.json | 9 ++ lang/pt.json | 9 ++ lang/zh.json | 15 +- lib/services/navigation_service.dart | 31 ++++ .../like_button_view_model.dart | 2 +- .../events/event_filter_bottomsheet.dart | 150 ++++++++++++++++++ .../events/explore_events.dart | 144 +++++++++-------- lib/widgets/multi_reaction.dart | 3 + .../navigation_service_test.dart | 9 +- .../events/create_event_page_test.dart | 8 + .../events/event_filter_bottomsheet_test.dart | 97 +++++++++++ .../events/explore_events_test.dart | 11 +- 17 files changed, 460 insertions(+), 74 deletions(-) create mode 100644 lib/views/after_auth_screens/events/event_filter_bottomsheet.dart create mode 100644 test/views/after_auth_screens/events/event_filter_bottomsheet_test.dart diff --git a/lang/de.json b/lang/de.json index 740a2fb83d..a68bed5a8c 100644 --- a/lang/de.json +++ b/lang/de.json @@ -89,6 +89,16 @@ "Cancel": "Abbrechen", "Done": "Fertig", "Explore Events": "Veranstaltungen erkunden", + "Filters": "Filter", + "Filter by Date": "Filtern nach Datum", + "All Events": "Alle Veranstaltungen", + "Show all events": "Alle Veranstaltungen anzeigen", + "Show all events created by you": "Alle Veranstaltungen anzeigen, die von Ihnen erstellt wurden", + "Registered Events": "Registrierte Veranstaltungen", + "Show all events you have registered": "Alle Veranstaltungen anzeigen, für die Sie sich registriert haben", + "Show events for all": "Alle Veranstaltungen anzeigen", + "Show invite-only events": "Nur Einladungsveranstaltungen anzeigen", + "Add Date": "Datum hinzufügen", "Event": "Vorfall", "My Events": "Meine Veranstaltungen", diff --git a/lang/en.json b/lang/en.json index c7cf4c0b77..2588cffeb7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -95,6 +95,15 @@ "Cancel": "Cancel", "Done": "Done", "Explore Events": "Explore Events", + "Filters": "Filters", + "Filter by Date": "Filter by Date", + "All Events": "All Events", + "Show all events": "Show all events", + "Show all events created by you": "Show all events created by you", + "Registered Events": "Registered Events", + "Show all events you have registered": "Show all events you have registered", + "Show events for all": "Show events for all", + "Show invite-only events": "Show invite-only events", "Add Date": "Add Date", "Event": "Event", "My Events": "My Events", diff --git a/lang/es.json b/lang/es.json index 9401739d42..6c6e705f3a 100644 --- a/lang/es.json +++ b/lang/es.json @@ -90,6 +90,15 @@ "Cancel": "Cancelar", "Done": "Hecho", "Explore Events": "Explorar eventos", + "Filters": "Filtros", + "Filter by Date": "Filtrar por fecha", + "All Events": "Todos los eventos", + "Show all events": "Mostrar todos los eventos", + "Show all events created by you": "Mostrar todos los eventos creados por ti", + "Registered Events": "Eventos registrados", + "Show all events you have registered": "Mostrar todos los eventos en los que te has registrado", + "Show events for all": "Mostrar eventos para todos", + "Show invite-only events": "Mostrar eventos solo por invitación", "Add Date": "Agregar fecha", "Event": "Evento", "My Events": "Mis Eventos", diff --git a/lang/fr.json b/lang/fr.json index 8bea3b769d..3ed1ff2ab6 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -95,6 +95,15 @@ "Cancel": "Annuler", "Done": "Fait", "Explore Events": "Explorer les événements", + "Filters": "Filtres", + "Filter by Date": "Filtrer par date", + "All Events": "Tous les événements", + "Show all events": "Afficher tous les événements", + "Show all events created by you": "Afficher tous les événements créés par vous", + "Registered Events": "Événements enregistrés", + "Show all events you have registered": "Afficher tous les événements auxquels vous êtes inscrit", + "Show events for all": "Afficher les événements pour tous", + "Show invite-only events": "Afficher les événements sur invitation uniquement", "Add Date": "Ajouter une date", "Event": "Événement", "My Events": "Mes événements", diff --git a/lang/hi.json b/lang/hi.json index 3e3ca23c4c..8ae54f08e5 100644 --- a/lang/hi.json +++ b/lang/hi.json @@ -90,6 +90,15 @@ "Cancel": "रद्द करना", "Done": "किया हुआ", "Explore Events": "घटनाओं का अन्वेषण करें", + "Filters": "फ़िल्टर", + "Filter by Date": "तारीख़ से फ़िल्टर करें", + "All Events": "सभी घटनाएँ", + "Show all events": "सभी घटनाएँ दिखाएं", + "Show all events created by you": "आपके द्वारा बनाई गई सभी घटनाएँ दिखाएं", + "Registered Events": "रजिस्टर की गई घटनाएँ", + "Show all events you have registered": "आपने जिन घटनाओं में पंजीकृत हुए हैं, उन सभी घटनाएँ दिखाएं", + "Show events for all": "सभी के लिए घटनाएँ दिखाएं", + "Show invite-only events": "आमंत्रित लोगों के लिए घटनाएँ दिखाएं", "Add Date": "तिथि जोड़ें", "Event": "प्रतिस्पर्धा", "My Events": "मेरे कार्यक्रम", diff --git a/lang/ja.json b/lang/ja.json index 22953de4f8..6a57e49f7c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -90,6 +90,15 @@ "Cancel": "キャンセル", "Done": "終わり", "Explore Events": "イベントを探索する", + "Filters": "フィルター", + "Filter by Date": "日付で絞り込む", + "All Events": "すべてのイベント", + "Show all events": "すべてのイベントを表示", + "Show all events created by you": "あなたが作成したすべてのイベントを表示", + "Registered Events": "登録済みイベント", + "Show all events you have registered": "あなたが登録したすべてのイベントを表示", + "Show events for all": "すべてのイベントを表示", + "Show invite-only events": "招待限定イベントを表示", "Add Date": "日付を追加", "Event": "イベント", "My Events": "私のイベント", diff --git a/lang/pt.json b/lang/pt.json index 1dafd483fa..1b156b3b68 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -90,6 +90,15 @@ "Cancel": "Cancelar", "Done": "Feita", "Explore Events": "Explorar eventos", + "Filters": "Filtros", + "Filter by Date": "Filtrar por data", + "All Events": "Todos os eventos", + "Show all events": "Mostrar todos os eventos", + "Show all events created by you": "Mostrar todos os eventos criados por você", + "Registered Events": "Eventos registrados", + "Show all events you have registered": "Mostrar todos os eventos em que você se registrou", + "Show events for all": "Mostrar eventos para todos", + "Show invite-only events": "Mostrar apenas eventos com convite", "Add Date": "Adicionar Data", "Event": "Evento", "My Events": "Meus Eventos", diff --git a/lang/zh.json b/lang/zh.json index 3f587f4cfe..42d7157e05 100644 --- a/lang/zh.json +++ b/lang/zh.json @@ -90,11 +90,20 @@ "Cancel": "取消", "Done": "完成", "Explore Events": "探索事件", - "Add Date": "添加日期", - "Event": "事件", + "Filters": "过滤器", + "Filter by Date": "按日期过滤", + "All Events": "所有活动", + "Show all events": "显示所有活动", "My Events": "我的活动", - "Public Events": "公共事件", + "Show all events created by you": "显示由您创建的所有活动", + "Registered Events": "已注册活动", + "Show all events you have registered": "显示您已注册的所有活动", + "Public Events": "公共活动", + "Show events for all": "显示所有人的活动", "Private Events": "私人活动", + "Show invite-only events": "仅显示邀请活动", + "Add Date": "添加日期", + "Event": "事件", "Liked by": "喜欢的人", "Comments": "评论", "FirstName LastName": "名字姓氏", diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index ae9260dbad..805bcc7680 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -16,6 +16,7 @@ import 'package:talawa/widgets/talawa_error_snackbar.dart'; /// * `showTalawaErrorDialog` /// * `pop` class NavigationService { + /// Key for Navigator State. GlobalKey navigatorKey = GlobalKey(); /// Pushes a Screen. @@ -106,6 +107,13 @@ class NavigationService { } /// This is used for the quick alert of `duration: 2 seconds` with text message(passed). + /// + /// **params**: + /// * `message`: Message would be shown on snackbar + /// * `duration`: Duration of Snackbar + /// + /// **returns**: + /// None void showSnackBar( String message, { Duration duration = const Duration(seconds: 2), @@ -119,6 +127,15 @@ class NavigationService { ); } + /// This is used for the quick error of `duration: 2 seconds`. + /// + /// **params**: + /// * `errorMessage`: Error Message shown in snackbar + /// * `messageType`: Type of Message + /// * `duration`: Duration of snackbar + /// + /// **returns**: + /// None void showTalawaErrorSnackBar( String errorMessage, MessageType messageType, @@ -138,6 +155,14 @@ class NavigationService { ); } + /// Shows an Error Dialog Box. + /// + /// **params**: + /// * `errorMessage`: Message shown in dialog + /// * `messageType`: Type of Message + /// + /// **returns**: + /// None void showTalawaErrorDialog(String errorMessage, MessageType messageType) { showDialog( context: navigatorKey.currentContext!, @@ -153,6 +178,12 @@ class NavigationService { } /// This function pops the current state. + /// + /// **params**: + /// None + /// + /// **returns**: + /// None void pop() { return navigatorKey.currentState!.pop(); } diff --git a/lib/view_model/widgets_view_models/like_button_view_model.dart b/lib/view_model/widgets_view_models/like_button_view_model.dart index f2ff2ce9fa..ee63f1e105 100644 --- a/lib/view_model/widgets_view_models/like_button_view_model.dart +++ b/lib/view_model/widgets_view_models/like_button_view_model.dart @@ -28,7 +28,7 @@ class LikeButtonViewModel extends BaseModel { // ignore: unused_field late StreamSubscription _updatePostSubscription; - // Getters + ///Getters. bool get isLiked => _isLiked; List get likedBy => _likedBy; int get likesCount => _likedBy.length; diff --git a/lib/views/after_auth_screens/events/event_filter_bottomsheet.dart b/lib/views/after_auth_screens/events/event_filter_bottomsheet.dart new file mode 100644 index 0000000000..a164521485 --- /dev/null +++ b/lib/views/after_auth_screens/events/event_filter_bottomsheet.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:talawa/apptheme.dart'; +import 'package:talawa/services/size_config.dart'; +import 'package:talawa/utils/app_localization.dart'; +import 'package:talawa/view_model/after_auth_view_models/event_view_models/explore_events_view_model.dart'; + +/// Shows a list of dropdown taken from `model` and `context`. +/// +/// **params**: +/// * `model`: contains the events data +/// * `context`: the overall context of UI +/// +/// **returns**: +/// * `Widget`: the dropdown +Widget dropDownList( + ExploreEventsViewModel model, + BuildContext context, +) { + final Map filters = { + 'All Events': 'Show all events', + 'My Events': 'Show all events created by you', + 'Registered Events': 'Show all events you have registered', + 'Public Events': 'Show events for all', + 'Private Events': 'Show invite-only events', + }; + return SizedBox( + height: SizeConfig.screenHeight, + width: SizeConfig.screenWidth, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: SizeConfig.safeBlockHorizontal! * 10, + ), + child: StatefulBuilder( + builder: (_, StateSetter setState) { + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: SizeConfig.safeBlockVertical, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of( + context, + )! + .strictTranslate( + "Filters", + ), + style: Theme.of( + context, + ).textTheme.headlineSmall, + ), + IconButton( + key: const Key('close'), + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + ), + ), + ], + ), + ...List.generate( + filters.length, + (index) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: SizeConfig.safeBlockVertical! * 2, + ), + child: GestureDetector( + onTap: () { + model.choseValueFromDropdown( + filters.keys.toList()[index], + ); + setState(() {}); + }, + child: Container( + key: Key( + filters.keys.toList()[index], + ), + decoration: BoxDecoration( + color: model.chosenValue == + filters.keys.toList()[index] + ? Theme.of(context).colorScheme.secondary + : AppTheme.white, + borderRadius: BorderRadius.all( + Radius.circular( + SizeConfig.safeBlockHorizontal! * 2, + ), + ), + ), + width: SizeConfig.screenWidth! - 60, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: SizeConfig.safeBlockHorizontal! * 5, + vertical: SizeConfig.safeBlockVertical! / 2, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.strictTranslate( + filters.keys.toList()[index], + ), + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith( + color: model.chosenValue == + filters.keys.toList()[index] + ? AppTheme.white + : AppTheme.blackPrimary, + ), + ), + Text( + AppLocalizations.of(context)!.strictTranslate( + filters.values.toList()[index], + ), + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith( + color: model.chosenValue == + filters.keys.toList()[index] + ? AppTheme.white + : AppTheme.blackSecondary, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ); +} diff --git a/lib/views/after_auth_screens/events/explore_events.dart b/lib/views/after_auth_screens/events/explore_events.dart index ed192eaaf4..cc9d665df7 100644 --- a/lib/views/after_auth_screens/events/explore_events.dart +++ b/lib/views/after_auth_screens/events/explore_events.dart @@ -5,6 +5,7 @@ import 'package:talawa/services/size_config.dart'; import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/view_model/after_auth_view_models/event_view_models/explore_events_view_model.dart'; import 'package:talawa/view_model/main_screen_view_model.dart'; +import 'package:talawa/views/after_auth_screens/events/event_filter_bottomsheet.dart'; import 'package:talawa/views/after_auth_screens/events/explore_event_dialogue.dart'; import 'package:talawa/views/base_view.dart'; import 'package:talawa/widgets/event_card.dart'; @@ -29,12 +30,16 @@ class ExploreEvents extends StatelessWidget { appBar: AppBar( // AppBar returns widget for the header. backgroundColor: Theme.of(context).primaryColor, - key: const Key("ExploreEventsAppBar"), + key: const Key( + "ExploreEventsAppBar", + ), elevation: 0.0, automaticallyImplyLeading: false, centerTitle: true, title: Text( - AppLocalizations.of(context)!.strictTranslate('Explore Events'), + AppLocalizations.of(context)!.strictTranslate( + 'Explore Events', + ), style: Theme.of(context).textTheme.titleLarge!.copyWith( fontWeight: FontWeight.w600, fontSize: 20, @@ -42,8 +47,12 @@ class ExploreEvents extends StatelessWidget { ), leading: IconButton( // returns a button of menu icon to redirect to home. - color: Theme.of(context).iconTheme.color, - icon: const Icon(Icons.menu), + color: Theme.of( + context, + ).iconTheme.color, + icon: const Icon( + Icons.menu, + ), onPressed: () => MainScreenViewModel.scaffoldKey.currentState!.openDrawer(), ), @@ -64,7 +73,10 @@ class ExploreEvents extends StatelessWidget { ), ); }, - icon: const Icon(Icons.search, size: 20), + icon: Icon( + Icons.search, + size: (SizeConfig.safeBlockHorizontal ?? 4) * 5, + ), ) : const SizedBox(), ), @@ -73,7 +85,9 @@ class ExploreEvents extends StatelessWidget { // if the model is still fetching the events list then renders the Circular Progress Indicator // else render refresh icon along with the list of searched events for exploration. body: model.isBusy - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: CircularProgressIndicator(), + ) : RefreshIndicator( onRefresh: () async => model.refreshEvents(), child: Stack( @@ -91,25 +105,56 @@ class ExploreEvents extends StatelessWidget { MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 3, - child: Card( - color: Theme.of(context) - .colorScheme - .onPrimary, - elevation: 2, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - //width: SizeConfig.screenWidth! * 0.45, - child: DropdownButtonHideUnderline( - child: dropDownList(model, context), + flex: 2, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + builder: (_) { + return dropDownList( + model, + context, + ); + }, + ); + }, + child: Card( + color: Theme.of(context) + .colorScheme + .onPrimary, + child: Container( + padding: EdgeInsets.symmetric( + vertical: (SizeConfig + .safeBlockHorizontal ?? + 4) * + 3, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: SizeConfig + .safeBlockHorizontal, + ), + Text( + AppLocalizations.of(context)! + .strictTranslate( + "Filters", + ), + ), + SizedBox( + width: SizeConfig + .safeBlockHorizontal, + ), + ], + ), ), ), ), ), Expanded( - flex: 2, + flex: 3, child: GestureDetector( onTap: () { showDialog( @@ -128,27 +173,39 @@ class ExploreEvents extends StatelessWidget { .colorScheme .onPrimary, child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12, + padding: EdgeInsets.symmetric( + vertical: (SizeConfig + .safeBlockHorizontal ?? + 4) * + 3, ), // width: SizeConfig.screenWidth! * 0.30, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + SizedBox( + width: SizeConfig + .safeBlockHorizontal, + ), const Icon( Icons.calendar_today, color: Color(0xff524F4F), ), - const SizedBox( - width: 8, + SizedBox( + width: SizeConfig + .safeBlockHorizontal, ), Text( AppLocalizations.of(context)! .strictTranslate( - "Add Date", + "Filter by Date", ), ), + SizedBox( + width: SizeConfig + .safeBlockHorizontal, + ), ], ), ), @@ -244,41 +301,4 @@ class ExploreEvents extends StatelessWidget { }, ); } - - /// Shows a list of dropdown taken from `model` and `context`. - /// - /// **params**: - /// * `model`: contains the events data - /// * `context`: the overall context of UI - /// - /// **returns**: - /// * `Widget`: the dropdown - Widget dropDownList(ExploreEventsViewModel model, BuildContext context) { - return DropdownButton( - key: homeModel?.keySECategoryMenu, - value: model.chosenValue, - isExpanded: true, - items: [ - 'All Events', - 'Created Events', - 'Registered Events', - 'Public Events', - 'Private Events', - ].map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - AppLocalizations.of(context)!.strictTranslate(value), - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).colorScheme.secondary), - ), - ); - }).toList(), - onChanged: (value) { - model.choseValueFromDropdown(value!); - }, - ); - } } diff --git a/lib/widgets/multi_reaction.dart b/lib/widgets/multi_reaction.dart index 2f054594c2..2abec4d95e 100644 --- a/lib/widgets/multi_reaction.dart +++ b/lib/widgets/multi_reaction.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_reaction_button/flutter_reaction_button.dart'; import 'package:flutter_svg/flutter_svg.dart'; +/// Reaction Button with multiple Emojis. class MultiReactButton extends StatefulWidget { const MultiReactButton({super.key, required this.toggle}); + + /// Toggle Function for react Button. final VoidCallback toggle; @override diff --git a/test/service_tests/navigation_service_test.dart b/test/service_tests/navigation_service_test.dart index bb361f3929..e40d8f17e8 100644 --- a/test/service_tests/navigation_service_test.dart +++ b/test/service_tests/navigation_service_test.dart @@ -8,7 +8,13 @@ import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/widgets/talawa_error_dialog.dart'; import 'package:talawa/widgets/talawa_error_snackbar.dart'; -// This methods tries to cover all the cases which can be there in future! +/// This methods tries to cover all the cases which can be there in future. +/// +/// **params**: +/// * `settings`: RouteSettings +/// +/// **returns**: +/// * `Route`: Returns Route Type Route _onGenerateTestRoute(RouteSettings settings) { if (settings.name == '/second-screen') { if (settings.arguments == null) { @@ -169,7 +175,6 @@ class _SecondTestScreenState extends State { class ThirdTestScreen extends StatefulWidget { const ThirdTestScreen({super.key, this.arguments}); final String? arguments; - @override State createState() => _ThirdTestScreenState(); } diff --git a/test/views/after_auth_screens/events/create_event_page_test.dart b/test/views/after_auth_screens/events/create_event_page_test.dart index 1cf338c8bf..1020f44d03 100644 --- a/test/views/after_auth_screens/events/create_event_page_test.dart +++ b/test/views/after_auth_screens/events/create_event_page_test.dart @@ -27,6 +27,14 @@ class MockCallbackFunction extends Mock { final setDateCallback = MockCallbackFunction(); final setTimeCallback = MockCallbackFunction(); +/// Creates a EventScreen for tests. +/// +/// **params**: +/// * `themeMode`: ThemeMode +/// * `theme`: ThemeData of App +/// +/// **returns**: +/// * `Widget`: Event Screen Widget Widget createEventScreen({ ThemeMode themeMode = ThemeMode.light, required ThemeData theme, diff --git a/test/views/after_auth_screens/events/event_filter_bottomsheet_test.dart b/test/views/after_auth_screens/events/event_filter_bottomsheet_test.dart new file mode 100644 index 0000000000..bd75c33020 --- /dev/null +++ b/test/views/after_auth_screens/events/event_filter_bottomsheet_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:talawa/router.dart' as router; +import 'package:talawa/services/size_config.dart'; +import 'package:talawa/services/third_party_service/multi_media_pick_service.dart'; +import 'package:talawa/utils/app_localization.dart'; +import 'package:talawa/view_model/after_auth_view_models/event_view_models/explore_events_view_model.dart'; +import 'package:talawa/view_model/main_screen_view_model.dart'; +import 'package:talawa/views/after_auth_screens/events/explore_events.dart'; +import 'package:talawa/widgets/custom_drawer.dart'; + +import '../../../helpers/test_helpers.dart'; +import '../../../helpers/test_locator.dart'; + +class MockCallbackFunction extends Mock { + void call(); +} + +final setDateCallback = MockCallbackFunction(); +final setTimeCallback = MockCallbackFunction(); + +/// Creates Explore Event Screen. +/// +/// **params**: +/// * `model`: Home Screen Model +/// +/// **returns**: +/// * `Widget`: Returns Explore Screen Widget +Widget createExploreEventsScreen(MainScreenViewModel model) => MaterialApp( + locale: const Locale('en'), + localizationsDelegates: [ + const AppLocalizationsDelegate(isTest: true), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + key: const Key('Root'), + home: Scaffold( + key: MainScreenViewModel.scaffoldKey, + drawer: CustomDrawer( + homeModel: model, + ), + body: const ExploreEvents( + key: Key('ExploreEvents'), + ), + ), + navigatorKey: navigationService.navigatorKey, + onGenerateRoute: router.generateRoute, + ); +void main() { + SizeConfig().test(); + testSetupLocator(); + locator.unregister(); + setUp(() { + registerServices(); + }); + tearDown(() { + unregisterServices(); + }); + group('testing filters bottomsheet', () { + testWidgets("Checking tap cross works", (tester) async { + await mockNetworkImages(() async { + locator.unregister(); + final model = ExploreEventsViewModel(); + locator.registerSingleton(model); + final homeModel = locator(); + await tester.pumpWidget(createExploreEventsScreen(homeModel)); + await tester.pumpAndSettle(); + await tester.tap(find.bySemanticsLabel('Filters')); + await tester.pumpAndSettle(); + final finder = find.byKey(const Key('close')); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + expect(find.bySemanticsLabel('Filters'), findsAtLeast(1)); + }); + }); + testWidgets("Testing if Filter button works", (tester) async { + await mockNetworkImages(() async { + locator.unregister(); + final model = ExploreEventsViewModel(); + locator.registerSingleton(model); + final homeModel = locator(); + await tester.pumpWidget(createExploreEventsScreen(homeModel)); + await tester.pumpAndSettle(); + await tester.tap(find.bySemanticsLabel('Filters')); + await tester.pumpAndSettle(); + await tester.pump(); + await tester.tap(find.byKey(const Key('Public Events'))); + await tester.pumpAndSettle(); + expect(model.chosenValue, 'Public Events'); + }); + }); + }); +} diff --git a/test/widget_tests/after_auth_screens/events/explore_events_test.dart b/test/widget_tests/after_auth_screens/events/explore_events_test.dart index d227dc2ecc..2d37da6cee 100644 --- a/test/widget_tests/after_auth_screens/events/explore_events_test.dart +++ b/test/widget_tests/after_auth_screens/events/explore_events_test.dart @@ -140,13 +140,12 @@ void main() { await tester.pumpWidget(createExploreEventsScreen(homeModel)); await tester.pumpAndSettle(); - await tester.tap(find.byType(DropdownButtonHideUnderline)); + await tester.tap(find.bySemanticsLabel('Filters')); await tester.pumpAndSettle(); - - await tester.tap(find.bySemanticsLabel('Created Events')); + await tester.pump(); + await tester.tap(find.byKey(const Key('Public Events'))); await tester.pumpAndSettle(); - - expect(model.chosenValue, 'Created Events'); + expect(model.chosenValue, 'Public Events'); }); }); testWidgets("Testing if tapping on calendar works", (tester) async { @@ -156,7 +155,7 @@ void main() { await tester.pumpWidget(createExploreEventsScreen(homeModel)); await tester.pumpAndSettle(); - await tester.tap(find.text('Add Date')); + await tester.tap(find.text('Filter by Date')); await tester.pump(); expect(find.byType(ExploreEventDialog), findsOneWidget);