From a66c8b69ca8614c3341dec0564309e1cb9bddb38 Mon Sep 17 00:00:00 2001 From: "mr.mojtaba" Date: Sun, 16 Nov 2025 15:40:21 +0330 Subject: [PATCH] test: add unit tests for poultry repository and searchable dropdown functionalities - Introduced tests for `PoultryScienceRepositoryImp` to validate delegated remote calls. - Added comprehensive tests for `SearchableDropdownLogic` covering selection, overlay, and search logic. - Enhanced `SearchableDropdown` widget tests for multi-select, label building, and overlay management. --- android/local.properties | 2 +- .../pages/system_design/system_design.dart | 11 - lib/presentation/pages/test/view.dart | 2 +- .../pages/steward/sales_in_province/view.dart | 140 +------------ .../widgets/cu_sale_in_provience.dart | 166 +++++++++++++++- .../steward/sales_out_of_province/logic.dart | 22 ++ .../steward/sales_out_of_province/view.dart | 26 ++- .../remote/auth/auth_remote_imp_test.dart | 2 +- .../auth/auth_repository_imp_test.dart | 41 +--- .../poultry_science_repository_test.dart | 145 ++++++++++++++ .../auth_flow_integration_test.dart | 90 +++------ .../presentation/widget/monthly_calender.dart | 138 ++++++++++--- .../multi_select_dropdown_logic.dart | 2 +- .../searchable_dropdown_logic_test.dart | 188 ++++++++++++++++++ .../searchable_dropdown_widget_test.dart | 139 +++++++++++++ pubspec.yaml | 2 +- 16 files changed, 812 insertions(+), 304 deletions(-) create mode 100644 packages/chicken/test/data/repositories/poultry_science/poultry_science_repository_test.dart create mode 100644 packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_logic_test.dart create mode 100644 packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_widget_test.dart diff --git a/android/local.properties b/android/local.properties index 99787f3..200b790 100644 --- a/android/local.properties +++ b/android/local.properties @@ -1,4 +1,4 @@ -sdk.dir=C:\\Users\\Housh11\\AppData\\Local\\Android\\sdk +sdk.dir=C:/Users/Housh11/AppData/Local/Android/Sdk flutter.sdk=C:\\src\\flutter flutter.buildMode=debug flutter.versionName=1.3.32 diff --git a/lib/presentation/pages/system_design/system_design.dart b/lib/presentation/pages/system_design/system_design.dart index bacf390..3f8f0cf 100644 --- a/lib/presentation/pages/system_design/system_design.dart +++ b/lib/presentation/pages/system_design/system_design.dart @@ -12,17 +12,6 @@ class SystemDesignPage extends StatefulWidget { class _SystemDesignPageState extends State { List _isOpen = [false, false, false, false, false, false]; - void _handleAdd() { - print("Add FAB pressed"); - } - - void _handleEdit() { - print("Edit FAB pressed"); - } - - void _handleDelete() { - print("Delete FAB pressed"); - } @override Widget build(BuildContext context) { diff --git a/lib/presentation/pages/test/view.dart b/lib/presentation/pages/test/view.dart index bec6b57..e8981b9 100644 --- a/lib/presentation/pages/test/view.dart +++ b/lib/presentation/pages/test/view.dart @@ -4,7 +4,7 @@ import 'package:rasadyar_core/core.dart'; import 'logic.dart'; class TestPage extends StatelessWidget { - const TestPage({Key? key}) : super(key: key); + const TestPage({super.key}); @override Widget build(BuildContext context) { diff --git a/packages/chicken/lib/presentation/pages/steward/sales_in_province/view.dart b/packages/chicken/lib/presentation/pages/steward/sales_in_province/view.dart index 3f46401..79b72a4 100644 --- a/packages/chicken/lib/presentation/pages/steward/sales_in_province/view.dart +++ b/packages/chicken/lib/presentation/pages/steward/sales_in_province/view.dart @@ -5,7 +5,7 @@ import 'package:rasadyar_chicken/presentation/utils/nested_keys_utils.dart'; import 'package:rasadyar_chicken/presentation/utils/string_utils.dart'; import 'package:rasadyar_chicken/presentation/widget/base_page/view.dart'; import 'package:rasadyar_chicken/presentation/widget/steward/inventory_widget.dart'; -import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_core/core.dart' hide modalDatePicker; import 'logic.dart'; @@ -412,12 +412,14 @@ class SalesInProvincePage extends GetView { children: [ Expanded( child: timeFilterWidget( + controller: controller, date: controller.fromDateFilter, onChanged: (jalali) => controller.fromDateFilter.value = jalali, ), ), Expanded( child: timeFilterWidget( + controller: controller, isFrom: false, date: controller.toDateFilter, onChanged: (jalali) => controller.toDateFilter.value = jalali, @@ -441,6 +443,7 @@ class SalesInProvincePage extends GetView { } GestureDetector timeFilterWidget({ + required SalesInProvinceLogic controller, isFrom = true, required Rx date, required Function(Jalali jalali) onChanged, @@ -482,139 +485,4 @@ class SalesInProvincePage extends GetView { ), ); } - - Container modalDatePicker(ValueChanged onDateSelected) { - Jalali? tempPickedDate; - return Container( - height: 250, - color: Colors.white, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - child: Row( - children: [ - SizedBox(width: 20), - RElevated( - height: 35, - width: 70, - textStyle: AppFonts.yekan14.copyWith(color: Colors.white), - onPressed: () { - onDateSelected(tempPickedDate ?? Jalali.now()); - Get.back(); - }, - text: 'تایید', - ), - Spacer(), - RElevated( - height: 35, - width: 70, - backgroundColor: AppColor.error, - textStyle: AppFonts.yekan14.copyWith(color: Colors.white), - onPressed: () { - onDateSelected(tempPickedDate ?? Jalali.now()); - Get.back(); - }, - text: 'لغو', - ), - SizedBox(width: 20), - ], - ), - ), - Divider(height: 0, thickness: 1), - Expanded( - child: Container( - child: PersianCupertinoDatePicker( - initialDateTime: Jalali.now(), - mode: PersianCupertinoDatePickerMode.date, - onDateTimeChanged: (dateTime) { - tempPickedDate = dateTime; - }, - ), - ), - ), - ], - ), - ); - } - - Widget show2StepAddBottomSheet() { - return BaseBottomSheet( - height: Get.height * .39, - child: Column( - spacing: 8, - children: [ - buildRow( - title: 'تاریخ ثبت', - value: controller.tmpStewardAllocation?.date?.formattedJalaliDate ?? 'ندارد', - ), - buildRow( - title: 'نام و نام خانوادگی خریدار', - value: - controller.guildsModel - .firstWhere((p0) => p0.key == controller.tmpStewardAllocation?.guildKey) - .user - ?.fullname ?? - 'ندارد', - ), - buildRow( - title: 'شماره خریدار', - value: - controller.guildsModel - .firstWhere((p0) => p0.key == controller.tmpStewardAllocation?.guildKey) - .user - ?.mobile ?? - 'ندارد', - ), - - buildRow( - title: 'قیمت هر کیلو', - value: '${controller.tmpStewardAllocation?.amount.separatedByCommaFa ?? 0} ریال ', - ), - buildRow( - title: 'وزن تخصیصی', - value: - '${controller.tmpStewardAllocation?.weightOfCarcasses?.toInt().separatedByCommaFa ?? 0} کیلوگرم', - ), - buildRow( - title: 'قیمت کل', - value: '${controller.tmpStewardAllocation?.totalAmount.separatedByCommaFa ?? 0} ریال', - ), - - Row( - spacing: 10, - children: [ - Expanded( - child: RElevated( - backgroundColor: AppColor.greenNormal, - height: 40, - text: 'ثبت', - textStyle: AppFonts.yekan18.copyWith(color: Colors.white), - onPressed: () async { - await controller.submitAllocation(); - Get - ..back() - ..back(); - }, - ), - ), - Expanded( - child: ROutlinedElevated( - height: 40, - borderColor: AppColor.error, - text: ' بازگشت', - textStyle: AppFonts.yekan18.copyWith(color: AppColor.error), - onPressed: () { - Get - ..back() - ..back(); - }, - ), - ), - ], - ), - ], - ), - ); - } } diff --git a/packages/chicken/lib/presentation/pages/steward/sales_in_province/widgets/cu_sale_in_provience.dart b/packages/chicken/lib/presentation/pages/steward/sales_in_province/widgets/cu_sale_in_provience.dart index 9f4e261..0d27602 100644 --- a/packages/chicken/lib/presentation/pages/steward/sales_in_province/widgets/cu_sale_in_provience.dart +++ b/packages/chicken/lib/presentation/pages/steward/sales_in_province/widgets/cu_sale_in_provience.dart @@ -31,15 +31,24 @@ Widget addOrEditBottomSheet(SalesInProvinceLogic controller, {bool isEditMode = spacing: 12, children: [ const SizedBox(height: 8), - RTextField( - controller: TextEditingController(), - filledColor: AppColor.bgLight, - label: 'تاریخ', - readonly: true, - borderColor: AppColor.darkGreyLight, - initText: Jalali.now().formatCompactDate(), - ), - + ObxValue((data) { + return RTextField( + controller: TextEditingController(), + filledColor: AppColor.bgLight, + filled: true, + label: 'تاریخ', + onTap: () { + Get.bottomSheet( + modalDatePicker((value) { + controller.fromDateFilter.value = value; + controller.fromDateFilter.refresh(); + }), + ); + }, + borderColor: AppColor.darkGreyLight, + initText: (data.value ?? Jalali.now()).formatCompactDate(), + ); + }, controller.fromDateFilter), Visibility( visible: isEditMode == false, child: Container( @@ -271,9 +280,11 @@ Widget addOrEditBottomSheet(SalesInProvinceLogic controller, {bool isEditMode = onPressed: isEditMode ? () async { await controller.updateAllocation(); + Get.back(); } : () async { await controller.submitAllocation(); + Get.back(); }, ); }, controller.isValid), @@ -365,3 +376,140 @@ Widget productDropDown(SalesInProvinceLogic controller) { ); }); } + +Container modalDatePicker(ValueChanged onDateSelected) { + Jalali? tempPickedDate; + return Container( + height: 250, + color: Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Row( + children: [ + SizedBox(width: 20), + RElevated( + height: 35, + width: 70, + textStyle: AppFonts.yekan14.copyWith(color: Colors.white), + onPressed: () { + onDateSelected(tempPickedDate ?? Jalali.now()); + Get.back(); + }, + text: 'تایید', + ), + Spacer(), + RElevated( + height: 35, + width: 70, + backgroundColor: AppColor.error, + textStyle: AppFonts.yekan14.copyWith(color: Colors.white), + onPressed: () { + onDateSelected(tempPickedDate ?? Jalali.now()); + Get.back(); + }, + text: 'لغو', + ), + SizedBox(width: 20), + ], + ), + ), + Divider(height: 0, thickness: 1), + Expanded( + child: Container( + child: PersianCupertinoDatePicker( + initialDateTime: Jalali.now(), + minimumDate: Jalali.now().add(days: -1), + maximumDate: Jalali.now(), + mode: PersianCupertinoDatePickerMode.date, + onDateTimeChanged: (dateTime) { + tempPickedDate = dateTime; + }, + ), + ), + ), + ], + ), + ); +} + +Widget show2StepAddBottomSheet(SalesInProvinceLogic controller) { + return BaseBottomSheet( + height: Get.height * .39, + child: Column( + spacing: 8, + children: [ + buildRow( + title: 'تاریخ ثبت', + value: controller.tmpStewardAllocation?.date?.formattedJalaliDate ?? 'ندارد', + ), + buildRow( + title: 'نام و نام خانوادگی خریدار', + value: + controller.guildsModel + .firstWhere((p0) => p0.key == controller.tmpStewardAllocation?.guildKey) + .user + ?.fullname ?? + 'ندارد', + ), + buildRow( + title: 'شماره خریدار', + value: + controller.guildsModel + .firstWhere((p0) => p0.key == controller.tmpStewardAllocation?.guildKey) + .user + ?.mobile ?? + 'ندارد', + ), + + buildRow( + title: 'قیمت هر کیلو', + value: '${controller.tmpStewardAllocation?.amount.separatedByCommaFa ?? 0} ریال ', + ), + buildRow( + title: 'وزن تخصیصی', + value: + '${controller.tmpStewardAllocation?.weightOfCarcasses?.toInt().separatedByCommaFa ?? 0} کیلوگرم', + ), + buildRow( + title: 'قیمت کل', + value: '${controller.tmpStewardAllocation?.totalAmount.separatedByCommaFa ?? 0} ریال', + ), + + Row( + spacing: 10, + children: [ + Expanded( + child: RElevated( + backgroundColor: AppColor.greenNormal, + height: 40, + text: 'ثبت', + textStyle: AppFonts.yekan18.copyWith(color: Colors.white), + onPressed: () async { + await controller.submitAllocation(); + Get + ..back() + ..back(); + }, + ), + ), + Expanded( + child: ROutlinedElevated( + height: 40, + borderColor: AppColor.error, + text: ' بازگشت', + textStyle: AppFonts.yekan18.copyWith(color: AppColor.error), + onPressed: () { + Get + ..back() + ..back(); + }, + ), + ), + ], + ), + ], + ), + ); +} diff --git a/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/logic.dart b/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/logic.dart index 7a272ab..0a53647 100644 --- a/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/logic.dart +++ b/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/logic.dart @@ -357,4 +357,26 @@ class SalesOutOfProvinceLogic extends GetxController { onError: (error, stacktrace) {}, ); } + + void setSaleDate(Jalali value) { + saleDate.value = value; + saleDate.refresh(); + dateErrorDialog(); + } + + void setProductionDate(DayInfo value) { + productionDate.value = value.date; + remainingStock.value = value.remainingStock; + dateErrorDialog(); + } + + void dateErrorDialog() { + if ((productionDate.value?.distanceTo(saleDate.value) ?? 0) >= 1) { + saleDate.value = Jalali.now(); + Future.delayed( + Duration(milliseconds: 300), + () => defaultShowErrorMessage("تاریخ تولید نمی تواند قبل از تاریخ فروش باشد"), + ); + } + } } diff --git a/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/view.dart b/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/view.dart index 1d448f2..ed43283 100644 --- a/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/view.dart +++ b/packages/chicken/lib/presentation/pages/steward/sales_out_of_province/view.dart @@ -352,14 +352,21 @@ class SalesOutOfProvincePage extends GetView { child: Column( spacing: 12, children: [ - RTextField( - controller: TextEditingController(), - filledColor: AppColor.bgLight, - label: 'تاریخ', - readonly: true, - borderColor: AppColor.darkGreyLight, - initText: Jalali.now().formatCompactDate(), - ), + ObxValue((data) { + return RTextField( + controller: TextEditingController(), + filledColor: AppColor.bgLight, + filled: true, + label: 'تاریخ', + onTap: () { + Get.bottomSheet( + modalDatePicker((value) => controller.setSaleDate(value)), + ); + }, + borderColor: AppColor.darkGreyLight, + initText: data.value.formatCompactDate(), + ); + }, controller.saleDate), Visibility( visible: isOnEdit == false, child: Container( @@ -430,8 +437,7 @@ class SalesOutOfProvincePage extends GetView { label: 'تاریخ تولید گوشت', selectedDate: controller.productionDate.value?.formatCompactDate(), onDateSelect: (value) { - controller.productionDate.value = value.date; - controller.remainingStock.value = value.remainingStock; + controller.setProductionDate(value); }, dayData: controller.quotaType.value == 1 ? controller.governmentalProductionDateData diff --git a/packages/chicken/test/data/data_source/remote/auth/auth_remote_imp_test.dart b/packages/chicken/test/data/data_source/remote/auth/auth_remote_imp_test.dart index 6a185c3..e616636 100644 --- a/packages/chicken/test/data/data_source/remote/auth/auth_remote_imp_test.dart +++ b/packages/chicken/test/data/data_source/remote/auth/auth_remote_imp_test.dart @@ -323,7 +323,7 @@ void main() { ).thenAnswer((_) async => mockResponse); // Act - await authRemoteDataSource.submitUserInfo(userInfo); + await authRemoteDataSource.stewardAppLogin(token: 'test-token', queryParameters: userInfo); // Assert verify( diff --git a/packages/chicken/test/data/repositories/auth/auth_repository_imp_test.dart b/packages/chicken/test/data/repositories/auth/auth_repository_imp_test.dart index c53076e..3dea869 100644 --- a/packages/chicken/test/data/repositories/auth/auth_repository_imp_test.dart +++ b/packages/chicken/test/data/repositories/auth/auth_repository_imp_test.dart @@ -146,45 +146,6 @@ void main() { }); }); - group('submitUserInfo', () { - test( - 'should call remote submitUserInfo with correct parameters', - () async { - // Arrange - const phone = '09123456789'; - const deviceName = 'Test Device'; - final expectedData = {'mobile': phone, 'device_name': deviceName}; - - when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - // Act - await authRepository.submitUserInfo( - phone: phone, - deviceName: deviceName, - ); - - // Assert - verify(() => mockAuthRemote.submitUserInfo(expectedData)).called(1); - }, - ); - - test('should call remote submitUserInfo without device name', () async { - // Arrange - const phone = '09123456789'; - final expectedData = {'mobile': phone, 'device_name': null}; - - when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - // Act - await authRepository.submitUserInfo(phone: phone); - - // Assert - verify(() => mockAuthRemote.submitUserInfo(expectedData)).called(1); - }); - }); + // submitUserInfo removed from API; covered by stewardAppLogin at remote layer }); } diff --git a/packages/chicken/test/data/repositories/poultry_science/poultry_science_repository_test.dart b/packages/chicken/test/data/repositories/poultry_science/poultry_science_repository_test.dart new file mode 100644 index 0000000..02d66ae --- /dev/null +++ b/packages/chicken/test/data/repositories/poultry_science/poultry_science_repository_test.dart @@ -0,0 +1,145 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:rasadyar_chicken/data/data_source/remote/poultry_science/poultry_science_remote.dart'; +import 'package:rasadyar_chicken/data/models/request/kill_registration/kill_registration.dart'; +import 'package:rasadyar_chicken/data/repositories/poultry_science/poultry_science_repository_imp.dart'; +import 'package:rasadyar_core/core.dart'; + +class _MockRemote extends Mock implements PoultryScienceRemoteDatasource {} + +void main() { + setUpAll(() { + registerFallbackValue(FormData()); + registerFallbackValue(const KillRegistrationRequest()); + registerFallbackValue({}); + }); + + group('PoultryScienceRepositoryImp', () { + late _MockRemote remote; + late PoultryScienceRepositoryImp repo; + + setUp(() { + remote = _MockRemote(); + repo = PoultryScienceRepositoryImp(remote); + }); + + test('getHomePoultry delegates', () async { + when(() => remote.getHomePoultryScience(token: any(named: 'token'), type: any(named: 'type'))) + .thenAnswer((_) async => null); + final res = await repo.getHomePoultry(token: 't', type: 'x'); + expect(res, null); + verify(() => remote.getHomePoultryScience(token: 't', type: 'x')).called(1); + }); + + test('getHatchingPoultry delegates', () async { + when(() => remote.getHatchingPoultry(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getHatchingPoultry(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getHatchingPoultry(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('submitPoultryScienceReport delegates', () async { + when(() => remote.submitPoultryScienceReport(token: any(named: 'token'), data: any(named: 'data'), onSendProgress: any(named: 'onSendProgress'))) + .thenAnswer((_) async {}); + await repo.submitPoultryScienceReport(token: 't', data: FormData()); + verify(() => remote.submitPoultryScienceReport(token: 't', data: any(named: 'data'), onSendProgress: any(named: 'onSendProgress'))).called(1); + }); + + test('getHatchingPoultryReport delegates', () async { + when(() => remote.getPoultryScienceReport(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getHatchingPoultryReport(token: 't', queryParameters: {'q': 1}); + expect(res, null); + verify(() => remote.getPoultryScienceReport(token: 't', queryParameters: {'q': 1})).called(1); + }); + + test('getPoultryScienceFarmList delegates', () async { + when(() => remote.getPoultryScienceFarmList(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getPoultryScienceFarmList(token: 't', queryParameters: {'p': 1}); + expect(res, null); + verify(() => remote.getPoultryScienceFarmList(token: 't', queryParameters: {'p': 1})).called(1); + }); + + test('getApprovedPrice delegates', () async { + when(() => remote.getApprovedPrice(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getApprovedPrice(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getApprovedPrice(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getAllPoultry delegates', () async { + when(() => remote.getAllPoultry(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getAllPoultry(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getAllPoultry(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getSellForFreezing delegates', () async { + when(() => remote.getSellForFreezing(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getSellForFreezing(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getSellForFreezing(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getPoultryExport delegates', () async { + when(() => remote.getPoultryExport(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getPoultryExport(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getPoultryExport(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getUserPoultry delegates', () async { + when(() => remote.getUserPoultry(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getUserPoultry(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getUserPoultry(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getPoultryHatching delegates', () async { + when(() => remote.getPoultryHatching(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getPoultryHatching(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getPoultryHatching(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('getKillHouseList delegates', () async { + when(() => remote.getKillHouseList(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getKillHouseList(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getKillHouseList(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('submitKillRegistration delegates', () async { + when(() => remote.submitKillRegistration(token: any(named: 'token'), request: any(named: 'request'))) + .thenAnswer((_) async {}); + await repo.submitKillRegistration(token: 't', request: const KillRegistrationRequest()); + verify(() => remote.submitKillRegistration(token: 't', request: any(named: 'request'))).called(1); + }); + + test('getPoultryOderList delegates', () async { + when(() => remote.getPoultryOderList(token: any(named: 'token'), queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => null); + final res = await repo.getPoultryOderList(token: 't', queryParameters: {'a': 1}); + expect(res, null); + verify(() => remote.getPoultryOderList(token: 't', queryParameters: {'a': 1})).called(1); + }); + + test('deletePoultryOder delegates', () async { + when(() => remote.deletePoultryOder(token: any(named: 'token'), orderId: any(named: 'orderId'))) + .thenAnswer((_) async {}); + await repo.deletePoultryOder(token: 't', orderId: 'id'); + verify(() => remote.deletePoultryOder(token: 't', orderId: 'id')).called(1); + }); + }); +} + + diff --git a/packages/chicken/test/integration/auth_flow_integration_test.dart b/packages/chicken/test/integration/auth_flow_integration_test.dart index c617939..a693e9f 100644 --- a/packages/chicken/test/integration/auth_flow_integration_test.dart +++ b/packages/chicken/test/integration/auth_flow_integration_test.dart @@ -4,7 +4,6 @@ import 'package:rasadyar_chicken/data/data_source/remote/auth/auth_remote.dart'; import 'package:rasadyar_chicken/data/models/response/user_info/user_info_model.dart'; import 'package:rasadyar_chicken/data/models/response/user_profile_model/user_profile_model.dart'; import 'package:rasadyar_chicken/data/repositories/auth/auth_repository_imp.dart'; -import 'package:rasadyar_core/core.dart'; class MockAuthRemoteDataSource extends Mock implements AuthRemoteDataSource {} @@ -22,7 +21,6 @@ void main() { test('should complete full login flow successfully', () async { // Arrange const phoneNumber = '09123456789'; - const deviceName = 'Test Device'; final authRequest = { 'username': 'test@example.com', 'password': 'password', @@ -48,28 +46,18 @@ void main() { // Mock the flow when( - () => mockAuthRemote.getUserInfo(phoneNumber), + () => mockAuthRemote.getUserInfo(phoneNumber), ).thenAnswer((_) async => expectedUserInfo); when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - when( - () => mockAuthRemote.login(authRequest: authRequest), + () => mockAuthRemote.login(authRequest: authRequest), ).thenAnswer((_) async => expectedUserProfile); // Act - Step 1: Get user info final userInfo = await authRepository.getUserInfo(phoneNumber); expect(userInfo, equals(expectedUserInfo)); - // Act - Step 2: Submit user info - await authRepository.submitUserInfo( - phone: phoneNumber, - deviceName: deviceName, - ); - - // Act - Step 3: Login + // Act - Step 2: Login final userProfile = await authRepository.login( authRequest: authRequest, ); @@ -77,7 +65,6 @@ void main() { // Assert expect(userProfile, equals(expectedUserProfile)); verify(() => mockAuthRemote.getUserInfo(phoneNumber)).called(1); - //verify(() => mockAuthRemote.submitUserInfo(any())).called(1); verify(() => mockAuthRemote.login(authRequest: authRequest)).called(1); }); @@ -100,11 +87,11 @@ void main() { // Mock the flow when( - () => mockAuthRemote.hasAuthenticated(), + () => mockAuthRemote.hasAuthenticated(), ).thenAnswer((_) async => false); when( - () => mockAuthRemote.login(authRequest: authRequest), + () => mockAuthRemote.login(authRequest: authRequest), ).thenAnswer((_) async => expectedUserProfile); // Act - Step 1: Check authentication status @@ -129,7 +116,7 @@ void main() { const phoneNumber = '09123456789'; when( - () => mockAuthRemote.getUserInfo(phoneNumber), + () => mockAuthRemote.getUserInfo(phoneNumber), ).thenAnswer((_) async => null); // Act @@ -143,7 +130,6 @@ void main() { test('should handle login failure after successful user info', () async { // Arrange const phoneNumber = '09123456789'; - const deviceName = 'Test Device'; final authRequest = { 'username': 'test@example.com', 'password': 'wrong', @@ -158,28 +144,18 @@ void main() { // Mock the flow when( - () => mockAuthRemote.getUserInfo(phoneNumber), + () => mockAuthRemote.getUserInfo(phoneNumber), ).thenAnswer((_) async => expectedUserInfo); when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - when( - () => mockAuthRemote.login(authRequest: authRequest), + () => mockAuthRemote.login(authRequest: authRequest), ).thenAnswer((_) async => null); // Act - Step 1: Get user info (success) final userInfo = await authRepository.getUserInfo(phoneNumber); expect(userInfo, equals(expectedUserInfo)); - // Act - Step 2: Submit user info (success) - await authRepository.submitUserInfo( - phone: phoneNumber, - deviceName: deviceName, - ); - - // Act - Step 3: Login (failure) + // Act - Step 2: Login (failure) final userProfile = await authRepository.login( authRequest: authRequest, ); @@ -187,7 +163,6 @@ void main() { // Assert expect(userProfile, isNull); verify(() => mockAuthRemote.getUserInfo(phoneNumber)).called(1); - verify(() => mockAuthRemote.submitUserInfo(any())).called(1); verify(() => mockAuthRemote.login(authRequest: authRequest)).called(1); }); }); @@ -209,7 +184,7 @@ void main() { test('should track authentication state correctly', () async { // Arrange when( - () => mockAuthRemote.hasAuthenticated(), + () => mockAuthRemote.hasAuthenticated(), ).thenAnswer((_) async => true); // Act @@ -223,7 +198,7 @@ void main() { test('should handle authentication state changes', () async { // Arrange when( - () => mockAuthRemote.hasAuthenticated(), + () => mockAuthRemote.hasAuthenticated(), ).thenAnswer((_) async => false); // Act @@ -236,40 +211,23 @@ void main() { }); group('User Info Management', () { - test('should handle user info submission without device name', () async { + test('should get user info by phone', () async { // Arrange const phone = '09123456789'; - final expectedData = {'mobile': phone, 'device_name': null}; - - when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - // Act - await authRepository.submitUserInfo(phone: phone); - - // Assert - verify(() => mockAuthRemote.submitUserInfo(expectedData)).called(1); - }); - - test('should handle user info submission with device name', () async { - // Arrange - const phone = '09123456789'; - const deviceName = 'Test Device'; - final expectedData = {'mobile': phone, 'device_name': deviceName}; - - when( - () => mockAuthRemote.submitUserInfo(any()), - ).thenAnswer((_) async {}); - - // Act - await authRepository.submitUserInfo( - phone: phone, - deviceName: deviceName, + final expectedUserInfo = UserInfoModel( + isUser: true, + address: 'Test Address', + backend: 'test-backend', + apiKey: 'test-api-key', ); - + when( + () => mockAuthRemote.getUserInfo(phone), + ).thenAnswer((_) async => expectedUserInfo); + // Act + final res = await authRepository.getUserInfo(phone); // Assert - verify(() => mockAuthRemote.submitUserInfo(expectedData)).called(1); + expect(res, expectedUserInfo); + verify(() => mockAuthRemote.getUserInfo(phone)).called(1); }); }); }); diff --git a/packages/core/lib/presentation/widget/monthly_calender.dart b/packages/core/lib/presentation/widget/monthly_calender.dart index b00bb90..b9e8092 100644 --- a/packages/core/lib/presentation/widget/monthly_calender.dart +++ b/packages/core/lib/presentation/widget/monthly_calender.dart @@ -58,7 +58,8 @@ class _MonthlyDataCalendarState extends State { @override void didUpdateWidget(MonthlyDataCalendar oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.selectedDate != widget.selectedDate || oldWidget.dayData != widget.dayData) { + if (oldWidget.selectedDate != widget.selectedDate || + oldWidget.dayData != widget.dayData) { _generateCalendar(); _updateDisplayValue(); } @@ -79,25 +80,31 @@ class _MonthlyDataCalendarState extends State { final twoDaysAgoStr = twoDaysAgo.formatCompactDate(); final oneDayAgoStr = oneDayAgo.formatCompactDate(); - return dateStr == todayStr || dateStr == twoDaysAgoStr || dateStr == oneDayAgoStr; + return dateStr == todayStr || + dateStr == twoDaysAgoStr || + dateStr == oneDayAgoStr; } void _generateCalendar() { + final days = _computeCalendarDays(); + setState(() { + _calendarDays = days; + }); + } + + List _computeCalendarDays() { final days = []; final year = _currentMonth.year; final month = _currentMonth.month; final daysInMonth = _currentMonth.monthLength; - // Get first day of month to determine starting position final firstDayOfMonth = Jalali(year, month, 1); - final dayOfWeek = firstDayOfMonth.weekDay; // 1 = Saturday in shamsi_date + final dayOfWeek = firstDayOfMonth.weekDay; - // Add empty cells for days before the first day of month for (int i = 1; i < dayOfWeek; i++) { days.add(null); } - // Add all days of the month for (int day = 1; day <= daysInMonth; day++) { final date = Jalali(year, month, day); final today = Jalali.now(); @@ -111,21 +118,23 @@ class _MonthlyDataCalendarState extends State { date: date, day: day, formattedDate: formattedDate, - isToday: date.year == today.year && date.month == today.month && date.day == today.day, + isToday: + date.year == today.year && + date.month == today.month && + date.day == today.day, isEnabled: isEnabled, hasZeroValue: hasZeroValue, remainingStock: data?.value ?? 0, ), ); } - - setState(() { - _calendarDays = days; - }); + return days; } void _handleDayClick(DayInfo dayInfo) { - if (dayInfo.isEnabled && !dayInfo.hasZeroValue && widget.onDateSelect != null) { + if (dayInfo.isEnabled && + !dayInfo.hasZeroValue && + widget.onDateSelect != null) { widget.onDateSelect!(dayInfo); Navigator.pop(context); } @@ -176,7 +185,8 @@ class _MonthlyDataCalendarState extends State { if (dayInfo != null) { final persianDay = _toPersianNumber(dayInfo.day); - _textController.text = '$persianDay ${_monthNames[dayInfo.date.month - 1]}'; + _textController.text = + '$persianDay ${_monthNames[dayInfo.date.month - 1]}'; } else { _textController.text = widget.selectedDate ?? ''; } @@ -198,23 +208,31 @@ class _MonthlyDataCalendarState extends State { context: context, builder: (BuildContext context) { return Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), child: Container( constraints: const BoxConstraints(maxWidth: 650, maxHeight: 650), child: Card( elevation: 3, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), child: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeader(), - const SizedBox(height: 16), - _buildDayNamesHeader(), - const SizedBox(height: 8), - _buildCalendarGrid(), - ], + child: StatefulBuilder( + builder: (context, setStateDialog) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeaderWithDialogSet(setStateDialog), + const SizedBox(height: 16), + _buildDayNamesHeader(), + const SizedBox(height: 8), + _buildCalendarGrid(), + ], + ); + }, ), ), ), @@ -224,6 +242,64 @@ class _MonthlyDataCalendarState extends State { ); } + Widget _buildHeaderWithDialogSet( + void Function(void Function()) setStateDialog, + ) { + return Container( + padding: const EdgeInsets.only(bottom: 16), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0), width: 2)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: () { + setStateDialog(() { + if (_currentMonth.month == 1) { + _currentMonth = Jalali(_currentMonth.year - 1, 12, 1); + } else { + _currentMonth = Jalali( + _currentMonth.year, + _currentMonth.month - 1, + 1, + ); + } + _calendarDays = _computeCalendarDays(); + }); + }, + icon: const Icon(Icons.chevron_left), + ), + Text( + '${_monthNames[_currentMonth.month - 1]} ${_toPersianNumber(_currentMonth.year)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Color(0xFF333333), + ), + ), + IconButton( + onPressed: () { + setStateDialog(() { + if (_currentMonth.month == 12) { + _currentMonth = Jalali(_currentMonth.year + 1, 1, 1); + } else { + _currentMonth = Jalali( + _currentMonth.year, + _currentMonth.month + 1, + 1, + ); + } + _calendarDays = _computeCalendarDays(); + }); + }, + icon: const Icon(Icons.chevron_right), + ), + ], + ), + ); + } + Widget _buildHeader() { return Container( padding: const EdgeInsets.only(bottom: 16), @@ -233,7 +309,10 @@ class _MonthlyDataCalendarState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton(onPressed: _handlePrevMonth, icon: const Icon(Icons.chevron_right)), + IconButton( + onPressed: _handlePrevMonth, + icon: const Icon(Icons.chevron_left), + ), Text( '${_monthNames[_currentMonth.month - 1]} ${_toPersianNumber(_currentMonth.year)}', style: const TextStyle( @@ -242,7 +321,10 @@ class _MonthlyDataCalendarState extends State { color: Color(0xFF333333), ), ), - IconButton(onPressed: _handleNextMonth, icon: const Icon(Icons.chevron_left)), + IconButton( + onPressed: _handleNextMonth, + icon: const Icon(Icons.chevron_right), + ), ], ), ); @@ -333,7 +415,9 @@ class _MonthlyDataCalendarState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, - color: dayInfo.isToday ? const Color(0xFFFF9800) : const Color(0xFF333333), + color: dayInfo.isToday + ? const Color(0xFFFF9800) + : const Color(0xFF333333), ), ), if (data != null && data.value != null) ...[ diff --git a/packages/core/lib/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart b/packages/core/lib/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart index 50b17d1..70610e6 100644 --- a/packages/core/lib/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart +++ b/packages/core/lib/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart @@ -96,7 +96,7 @@ class SearchableDropdownLogic extends GetxController { if (selectedItem != null) { this.selectedItem.value = selectedItem; } else { - this.selectedItem.value = initialValue != null ? [?initialValue] : []; + this.selectedItem.value = initialValue != null ? [initialValue as T] : []; } } diff --git a/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_logic_test.dart b/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_logic_test.dart new file mode 100644 index 0000000..217cdcf --- /dev/null +++ b/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_logic_test.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:rasadyar_core/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SearchableDropdownLogic', () { + test('initializes selectedItem from provided list', () { + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + selectedItem: const ['A'], + itemBuilder: (i) => Text(i), + ); + expect(logic.selectedItem, ['A']); + }); + + test('initializes selectedItem from initialValue when single select', () { + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + singleSelect: true, + initialValue: 'B', + itemBuilder: (i) => Text(i), + ); + expect(logic.selectedItem, ['B']); + }); + + testWidgets('showOverlay sets isOpen and inserts overlay', (tester) async { + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + itemBuilder: (i) => Text(i), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return CompositedTransformTarget( + link: logic.layerLink, + child: GestureDetector( + onTap: () => logic.showOverlay(context), + child: const SizedBox(width: 200, height: 40), + ), + ); + }, + ), + ), + ), + ); + expect(logic.isOpen.value, false); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + expect(logic.isOpen.value, true); + expect(find.text('نتیجه‌ای یافت نشد.'), findsNothing); + }); + + testWidgets('removeOverlay resets isOpen', (tester) async { + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + itemBuilder: (i) => Text(i), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return CompositedTransformTarget( + link: logic.layerLink, + child: GestureDetector( + onTap: () { + if (!logic.isOpen.value) { + logic.showOverlay(context); + } else { + logic.removeOverlay(); + } + }, + child: const SizedBox(width: 200, height: 40), + ), + ); + }, + ), + ), + ), + ); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + expect(logic.isOpen.value, true); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + expect(logic.isOpen.value, false); + }); + + testWidgets('tap item adds to selected and calls onChanged', (tester) async { + String? changed; + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + onChanged: (s) => changed = s, + itemBuilder: (i) => Text(i), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return CompositedTransformTarget( + link: logic.layerLink, + child: GestureDetector( + onTap: () => logic.showOverlay(context), + child: const SizedBox(width: 200, height: 40), + ), + ); + }, + ), + ), + ), + ); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + await tester.tap(find.text('A')); + await tester.pump(); + expect(logic.selectedItem.contains('A'), true); + expect(changed, 'A'); + }); + + testWidgets('singleSelect updates searchController text using labelBuilder', (tester) async { + final logic = SearchableDropdownLogic( + items: const ['A', 'B'], + singleSelect: true, + labelBuilder: (s) => 'L:$s', + itemBuilder: (i) => Text(i), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return CompositedTransformTarget( + link: logic.layerLink, + child: GestureDetector( + onTap: () => logic.showOverlay(context), + child: const SizedBox(width: 200, height: 40), + ), + ); + }, + ), + ), + ), + ); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + await tester.tap(find.text('B')); + await tester.pump(); + expect(logic.searchController.text, 'L:B'); + }); + + testWidgets('performSearch uses onSearch and updates filteredItems', (tester) async { + final logic = SearchableDropdownLogic( + items: const ['A', 'B', 'C'], + onSearch: (q) async => ['B'], + itemBuilder: (i) => Text(i), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return CompositedTransformTarget( + link: logic.layerLink, + child: GestureDetector( + onTap: () => logic.showOverlay(context), + child: const SizedBox(width: 200, height: 40), + ), + ); + }, + ), + ), + ), + ); + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + logic.performSearch('x'); + await tester.pumpAndSettle(); + expect(logic.filteredItems, ['B']); + }); + }); +} + + diff --git a/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_widget_test.dart b/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_widget_test.dart new file mode 100644 index 0000000..3c836c7 --- /dev/null +++ b/packages/core/test/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/searchable_dropdown_widget_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:rasadyar_core/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SearchableDropdown widget', () { + testWidgets('asserts builders based on singleSelect', (tester) async { + expect( + () => SearchableDropdown( + items: const ['A', 'B'], + itemBuilder: Text.new, + singleSelect: true, + ), + throwsA(isA()), + ); + expect( + () => SearchableDropdown( + items: const ['A', 'B'], + itemBuilder: Text.new, + singleSelect: false, + ), + throwsA(isA()), + ); + }); + + testWidgets('single select: selecting item sets text and calls onChanged', (tester) async { + String? changed; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchableDropdown( + items: const ['A', 'B'], + singleSelect: true, + singleLabelBuilder: (s) => 'L:$s', + itemBuilder: Text.new, + onChanged: (v) => changed = v, + ), + ), + ), + ); + await tester.tap(find.byType(TextField)); + await tester.pump(); + await tester.tap(find.text('A')); + await tester.pump(); + final tf = tester.widget(find.byType(TextField)); + expect(tf.controller!.text, 'L:A'); + expect(changed, 'A'); + }); + + testWidgets('multi select: shows chips and allows removing selection', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchableDropdown( + items: const ['A', 'B', 'C'], + singleSelect: false, + multiLabelBuilder: (s) => Container( + key: Key('chip_${s ?? ''}'), + padding: const EdgeInsets.all(4), + child: Text(s ?? ''), + ), + itemBuilder: Text.new, + ), + ), + ), + ); + await tester.tap(find.byType(TextField)); + await tester.pump(); + await tester.tap(find.text('A')); + await tester.pump(); + await tester.tap(find.byType(TextField)); + await tester.pump(); + await tester.tap(find.text('B')); + await tester.pump(); + expect(find.byKey(const Key('chip_A')), findsOneWidget); + expect(find.byKey(const Key('chip_B')), findsOneWidget); + await tester.tap(find.byKey(const Key('chip_A'))); + await tester.pump(); + expect(find.byKey(const Key('chip_A')), findsNothing); + expect(find.byKey(const Key('chip_B')), findsOneWidget); + }); + + testWidgets('readOnly toggles based on onSearch presence', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + children: [ + SearchableDropdown( + items: const ['A'], + singleSelect: true, + singleLabelBuilder: (s) => s ?? '', + itemBuilder: Text.new, + ), + SearchableDropdown( + items: const ['A'], + singleSelect: true, + singleLabelBuilder: (s) => s ?? '', + itemBuilder: Text.new, + onSearch: (q) async => ['A'], + ), + ], + ), + ), + ), + ); + final textFields = tester.widgetList(find.byType(TextField)).toList(); + expect(textFields[0].readOnly, true); + expect(textFields[1].readOnly, false); + }); + + testWidgets('tapping TextField toggles overlay list visibility', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchableDropdown( + items: const ['A', 'B'], + singleSelect: true, + singleLabelBuilder: (s) => s ?? '', + itemBuilder: Text.new, + ), + ), + ), + ); + expect(find.text('A'), findsNothing); + await tester.tap(find.byType(TextField)); + await tester.pump(); + expect(find.text('A'), findsOneWidget); + await tester.tap(find.text('A')); + await tester.pump(); + expect(find.text('A'), findsNothing); + }); + }); +} + + diff --git a/pubspec.yaml b/pubspec.yaml index 1f506bd..65b7df0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: rasadyar_app description: "A new Flutter project." publish_to: 'none' -version: 1.3.32+29 +version: 1.3.33+30 environment: sdk: ^3.9.2