From 4538959d30a743b692a4cade362cefdda3fc352c Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 2 Apr 2025 11:25:06 +0700 Subject: [PATCH] TF-3578 Calendar event show free busy status --- contact/pubspec.lock | 2 +- .../extensions/color_extension.dart | 1 + email_recovery/pubspec.lock | 2 +- fcm/pubspec.lock | 2 +- forward/pubspec.lock | 2 +- .../data/network/calendar_event_api.dart | 44 +++ .../calendar_event_repository_impl.dart | 1 + .../controller/single_email_controller.dart | 1 + .../email/presentation/email_view.dart | 1 + .../model/blob_calendar_event.dart | 9 +- .../event_time_information_widget_styles.dart | 2 + .../calendar_event_information_widget.dart | 7 +- .../event_time_information_widget.dart | 29 +- lib/l10n/intl_messages.arb | 6 + lib/main/localizations/app_localizations.dart | 7 + model/pubspec.lock | 2 +- pubspec.lock | 2 +- rule_filter/pubspec.lock | 2 +- server_settings/pubspec.lock | 2 +- .../data/network/calendar_event_api_test.dart | 277 ++++++++++++++++++ .../event_time_information_widget_test.dart | 70 +++++ 21 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 test/features/email/data/network/calendar_event_api_test.dart create mode 100644 test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index c9ddc3944..ae6919613 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -653,7 +653,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index dc8e03277..4a7f17f79 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -247,6 +247,7 @@ extension AppColor on Color { static const m3Neutral70 = Color(0xFFAEAAAE); static const grayBackgroundColor = Color(0xFFF3F6F9); static const m3SurfaceBackground = Color(0xFF1C1B1F); + static const warningColor = Color(0xFFFFC107); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock index 368b0e476..cde1eb3ab 100644 --- a/email_recovery/pubspec.lock +++ b/email_recovery/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index 329ea1540..13c974fe0 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/forward/pubspec.lock b/forward/pubspec.lock index 329ea1540..13c974fe0 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/lib/features/email/data/network/calendar_event_api.dart b/lib/features/email/data/network/calendar_event_api.dart index 91c4ddb1c..ba3adf82e 100644 --- a/lib/features/email/data/network/calendar_event_api.dart +++ b/lib/features/email/data/network/calendar_event_api.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/response/response_object.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/attendance/get_calendar_event_attendance_method.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/attendance/get_calendar_event_attendance_response.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_method.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_response.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_method.dart'; @@ -23,8 +28,20 @@ class CalendarEventAPI { Future> parse(AccountId accountId, Set blobIds) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + + // Parse final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds); final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod); + + // Free/Busy query + final calendarEventAttendanceGetMethod = GetCalendarEventAttendanceMethod( + accountId, + blobIds.toList(), + ); + final calendarEventAttendanceGetInvocation = requestBuilder.invocation( + calendarEventAttendanceGetMethod, + ); + final response = await (requestBuilder ..usings(calendarEventParseMethod.requiredCapabilities)) .build() @@ -33,11 +50,24 @@ class CalendarEventAPI { final calendarEventParseResponse = response.parse( calendarEventParseInvocation.methodCallId, CalendarEventParseResponse.deserialize); + final calendarEventAttendanceGetResponse = _parseCalendarEventAttendance( + response, + calendarEventAttendanceGetInvocation.methodCallId, + ); + final calendarBlobIdStatusMap = Map.fromEntries( + (calendarEventAttendanceGetResponse?.list ?? []).map( + (calendarEventAttendance) => MapEntry( + calendarEventAttendance.blobId, + calendarEventAttendance.isFree, + ), + ), + ); if (calendarEventParseResponse?.parsed?.isNotEmpty == true) { return calendarEventParseResponse!.parsed!.entries .map((entry) => BlobCalendarEvent( blobId: entry.key, + isFree: calendarBlobIdStatusMap[entry.key] ?? true, calendarEventList: entry.value)) .toList(); } else if (calendarEventParseResponse?.notParsable?.isNotEmpty == true) { @@ -49,6 +79,20 @@ class CalendarEventAPI { } } + GetCalendarEventAttendanceResponse? _parseCalendarEventAttendance( + ResponseObject response, + MethodCallId methodCallId, + ) { + try { + return response.parse( + methodCallId, + GetCalendarEventAttendanceResponse.deserialize); + } catch (e) { + logError('CalendarEventAPI.parse free/busy query error: $e'); + return null; + } + } + Future acceptEventInvitation( AccountId accountId, Set blobIds, diff --git a/lib/features/email/data/repository/calendar_event_repository_impl.dart b/lib/features/email/data/repository/calendar_event_repository_impl.dart index c0e48e417..8c5df318a 100644 --- a/lib/features/email/data/repository/calendar_event_repository_impl.dart +++ b/lib/features/email/data/repository/calendar_event_repository_impl.dart @@ -66,6 +66,7 @@ class CalendarEventRepositoryImpl extends CalendarEventRepository { calendarEventList: await Future.wait(blobCalendarEvent.calendarEventList.map((calendarEvent) { return _transformCalendarEventDescription(calendarEvent, transformConfiguration); })), + isFree: blobCalendarEvent.isFree, ); })); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index f8f86ba93..a47f7fc64 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -211,6 +211,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { CalendarEvent? get calendarEvent => blobCalendarEvent.value?.calendarEventList.firstOrNull; Id? get _displayingEventBlobId => blobCalendarEvent.value?.blobId; + bool get isCalendarEventFree => blobCalendarEvent.value?.isFree ?? true; AccountId? get accountId => mailboxDashBoardController.accountId.value; diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 4b68b2bd6..aaf9ef2b8 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -371,6 +371,7 @@ class EmailView extends GetWidget { presentationEmail: controller.currentEmail, onMailtoAttendeesAction: controller.handleMailToAttendees, openEmailAddressDetailAction: controller.openEmailAddressDialog, + isFree: controller.isCalendarEventFree, )), if (_validateDisplayEventActionBanner( context: context, diff --git a/lib/features/email/presentation/model/blob_calendar_event.dart b/lib/features/email/presentation/model/blob_calendar_event.dart index 5a333ef86..9fba0be5e 100644 --- a/lib/features/email/presentation/model/blob_calendar_event.dart +++ b/lib/features/email/presentation/model/blob_calendar_event.dart @@ -5,9 +5,14 @@ import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; class BlobCalendarEvent with EquatableMixin { final Id blobId; final List calendarEventList; + final bool isFree; - BlobCalendarEvent({required this.blobId, required this.calendarEventList}); + BlobCalendarEvent({ + required this.blobId, + required this.calendarEventList, + this.isFree = true, + }); @override - List get props => [blobId, calendarEventList]; + List get props => [blobId, calendarEventList, isFree]; } \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart index 874e389f2..47983e926 100644 --- a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart +++ b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; class EventTimeInformationWidgetStyles { static const double maxWidth = 100; static const double textSize = 16; + static const double horizontalSpacing = 8; static const Color labelColor = AppColor.colorSubTitleEventActionText; static const Color valueColor = Colors.black; + static const Color conflictColor = AppColor.warningColor; } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index 798a47620..4f4877cf1 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -32,6 +32,7 @@ class CalendarEventInformationWidget extends StatelessWidget { final PresentationEmail? presentationEmail; final OnMailtoAttendeesAction? onMailtoAttendeesAction; final OnOpenEmailAddressDetailAction? openEmailAddressDetailAction; + final bool isFree; final _responsiveUtils = Get.find(); @@ -40,6 +41,7 @@ class CalendarEventInformationWidget extends StatelessWidget { required this.calendarEvent, required this.onCalendarEventReplyActionClick, required this.calendarEventReplying, + required this.isFree, this.onOpenNewTabAction, this.onOpenComposerAction, this.presentationEmail, @@ -250,7 +252,10 @@ class CalendarEventInformationWidget extends StatelessWidget { if (dateTimeEvent.isNotEmpty) { return Padding( padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), - child: EventTimeInformationWidget(timeEvent: dateTimeEvent), + child: EventTimeInformationWidget( + timeEvent: dateTimeEvent, + isFree: isFree, + ), ); } else { return const SizedBox.shrink(); diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart index 1130e5a23..eb75d74bc 100644 --- a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart @@ -1,7 +1,10 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/event_time_information_widget_styles.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -9,17 +12,21 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class EventTimeInformationWidget extends StatelessWidget { final String timeEvent; + final bool isFree; const EventTimeInformationWidget({ super.key, - required this.timeEvent + required this.timeEvent, + required this.isFree, }); @override Widget build(BuildContext context) { final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: EventTimeInformationWidgetStyles.maxWidth, @@ -32,7 +39,7 @@ class EventTimeInformationWidget extends StatelessWidget { ), ), ), - Expanded(child: Text( + Flexible(child: Text( timeEvent, overflow: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultTextOverFlow, softWrap: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultSoftWrap, @@ -42,7 +49,21 @@ class EventTimeInformationWidget extends StatelessWidget { fontWeight: FontWeight.w500, color: EventTimeInformationWidgetStyles.valueColor ), - )) + )), + if (!isFree) + ...[ + const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing), + Tooltip( + message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime, + child: SvgPicture.asset( + imagePaths.icError, + width: 20, + height: 20, + fit: BoxFit.fill, + colorFilter: EventTimeInformationWidgetStyles.conflictColor.asFilter(), + ), + ), + ] ], ); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b1b9fd0c5..dfc59c144 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4370,6 +4370,12 @@ "placeholders_order": [], "placeholders": {} }, + "youHaveAnotherEventAtThatSameTime": "You have another event at that same time", + "@youHaveAnotherEventAtThatSameTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "exitFullscreen": "Exit fullscreen", "@exitFullscreen": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index a3c395a69..12b91dfdf 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4591,6 +4591,13 @@ class AppLocalizations { ); } + String get youHaveAnotherEventAtThatSameTime { + return Intl.message( + 'You have another event at that same time', + name: 'youHaveAnotherEventAtThatSameTime', + ); + } + String get exitFullscreen { return Intl.message( 'Exit fullscreen', diff --git a/model/pubspec.lock b/model/pubspec.lock index b45d5351c..7a38fb772 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -645,7 +645,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/pubspec.lock b/pubspec.lock index ee51633d4..cb53b5f9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1325,7 +1325,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index 329ea1540..13c974fe0 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/server_settings/pubspec.lock b/server_settings/pubspec.lock index eba1996bf..e4f7c775f 100644 --- a/server_settings/pubspec.lock +++ b/server_settings/pubspec.lock @@ -288,7 +288,7 @@ packages: description: path: "." ref: main - resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08" + resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.3" diff --git a/test/features/email/data/network/calendar_event_api_test.dart b/test/features/email/data/network/calendar_event_api_test.dart new file mode 100644 index 000000000..241027b95 --- /dev/null +++ b/test/features/email/data/network/calendar_event_api_test.dart @@ -0,0 +1,277 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import 'calendar_event_api_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CalendarEventAPI::test', () { + late HttpClient httpClient; + late CalendarEventAPI calendarEventAPI; + + setUp(() { + httpClient = MockHttpClient(); + calendarEventAPI = CalendarEventAPI(httpClient); + }); + + group('parse::test', () { + test('SHOULD return list of BlobCalendarEvent WHEN parse method success', () async { + final blobIds = {Id('blob_1'), Id('blob_2')}; + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "parsed": { + "blob_1": [ + { + "uid": "event1", + "title": "Meeting 1", + "start": "2024-03-20T10:00:00Z", + "duration": "PT1H" + } + ], + "blob_2": [ + { + "uid": "event2", + "title": "Meeting 2", + "start": "2024-03-21T14:00:00Z", + "duration": "PT2H" + } + ] + } + }, + "c0" + ], + [ + "CalendarEventAttendance/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "list": [ + { + "blobId": "blob_1", + "isFree": true + }, + { + "blobId": "blob_2", + "isFree": false + } + ] + }, + "c1" + ] + ] + }); + + final result = await calendarEventAPI.parse( + AccountFixtures.aliceAccountId, + blobIds + ); + + verify(httpClient.post( + '', + data: { + "using": [ + "urn:ietf:params:jmap:core", + "com:linagora:params:calendar:event", + ], + "methodCalls": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "blobIds": ["blob_1", "blob_2"] + }, + "c0" + ], + [ + "CalendarEventAttendance/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "blobIds": ["blob_1", "blob_2"] + }, + "c1" + ] + ] + }, + cancelToken: anyNamed('cancelToken'), + )).called(1); + + expect(result, isA>()); + expect(result.length, 2); + expect(result[0].blobId.value, 'blob_1'); + expect(result[0].isFree, true); + expect(result[1].blobId.value, 'blob_2'); + expect(result[1].isFree, false); + }); + + test('SHOULD throw NotParsableCalendarEventException WHEN response has notParsable', () async { + final blobIds = {Id('blob_1')}; + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "notParsable": ["blob_1"] + }, + "c0" + ], + [ + "CalendarEventAttendance/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "list": [] + }, + "c1" + ] + ] + }); + + expect( + () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds), + throwsA(isA()) + ); + }); + + test('SHOULD throw NotFoundCalendarEventException WHEN response has notFound', () async { + final blobIds = {Id('blob_1')}; + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "notFound": ["blob_1"] + }, + "c0" + ], + [ + "CalendarEventAttendance/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "list": [] + }, + "c1" + ] + ] + }); + + expect( + () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds), + throwsA(isA()) + ); + }); + + test('SHOULD throw NotParsableCalendarEventException WHEN response is empty', () async { + final blobIds = {Id('blob_1')}; + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString + }, + "c0" + ], + [ + "CalendarEventAttendance/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "list": [] + }, + "c1" + ] + ] + }); + + expect( + () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds), + throwsA(isA()) + ); + }); + + test('SHOULD return calendar events with default isFree value WHEN CalendarEventAttendance/get returns error', () async { + final blobIds = {Id('blob_1')}; + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "CalendarEvent/parse", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "parsed": { + "blob_1": [ + { + "uid": "event1", + "title": "Meeting 1", + "start": "2024-03-20T10:00:00Z", + "duration": "PT1H" + } + ] + } + }, + "c0" + ], + [ + "error", + { + "type": "invalidArguments", + "description": "Invalid blobIds" + }, + "c1" + ] + ] + }); + + final result = await calendarEventAPI.parse( + AccountFixtures.aliceAccountId, + blobIds + ); + + expect(result, isA>()); + expect(result.length, 1); + expect(result[0].blobId.value, 'blob_1'); + expect(result[0].isFree, true); + }); + }); + }); +} \ No newline at end of file diff --git a/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart b/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart new file mode 100644 index 000000000..88af3deda --- /dev/null +++ b/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/resources/image_paths.dart'; + +import '../../../../../fixtures/widget_fixtures.dart'; + +void main() { + setUp(() { + Get.put(ResponsiveUtils()); + Get.put(ImagePaths()); + }); + + tearDown(() { + Get.reset(); + }); + + group('EventTimeInformationWidget tests', () { + Widget makeTestableWidget({ + required String timeEvent, + required bool isFree, + }) { + return WidgetFixtures.makeTestableWidget( + child: EventTimeInformationWidget( + timeEvent: timeEvent, + isFree: isFree, + ), + ); + } + + testWidgets('should show error icon with tooltip when isFree is false', (tester) async { + // arrange + final widget = makeTestableWidget( + timeEvent: '10:00 AM - 11:00 AM', + isFree: false, + ); + + // act + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + + // assert + final svgFinder = find.byWidgetPredicate((widget) { + return widget is SvgPicture + && (widget.bytesLoader as SvgAssetLoader).assetName == Get.find().icError; + }); + expect(svgFinder, findsOneWidget); + expect(find.byType(Tooltip), findsOneWidget); + }); + + testWidgets('should not show error icon when isFree is true', (tester) async { + // arrange + final widget = makeTestableWidget( + timeEvent: '10:00 AM - 11:00 AM', + isFree: true, + ); + + // act + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + + // assert + expect(find.byType(SvgPicture), findsNothing); + expect(find.byType(Tooltip), findsNothing); + }); + }); +} \ No newline at end of file