TF-3804 Thread Detail Load selected email in parallel with thread metadata

This commit is contained in:
DatDang
2025-06-20 10:10:24 +07:00
committed by Dat H. Pham
parent 683d407fba
commit 5fd4986c6a
19 changed files with 863 additions and 421 deletions
@@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
@@ -18,55 +19,155 @@ import 'handle_get_email_ids_by_thread_id_success_test.mocks.dart';
])
void main() {
final threadDetailController = MockThreadDetailController();
final mailboxDashBoardController = MockMailboxDashBoardController();
group('handle get email ids by thread id success test:', () {
test(
'should assign emailIds with result from success '
'when result from success is not empty',
'should not change emailIdsPresentation '
'when GetThreadByIdSuccess.success.emailIds is empty',
() {
// arrange
final success = GetThreadByIdSuccess([
EmailId(Id('1')),
EmailId(Id('2')),
]);
final mailboxDashBoardController = MockMailboxDashBoardController();
when(threadDetailController.emailIdsPresentation).thenReturn(
<EmailId, PresentationEmail?>{}.obs,
);
when(mailboxDashBoardController.selectedEmail).thenReturn(
Rxn(PresentationEmail(id: EmailId(Id('1')))),
);
when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
final initialEmailIdsPresentation = {
EmailId(Id('1')): PresentationEmail(),
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(success);
// assert
expect(threadDetailController.emailIdsPresentation.keys, success.emailIds);
});
test(
'should assign emailIds with result from mailbox dashboard controller '
'when result from success is empty '
'and mailbox dashboard controller selected email is not null',
() {
// arrange
final mailboxDashBoardController = MockMailboxDashBoardController();
final success = GetThreadByIdSuccess([]);
when(mailboxDashBoardController.selectedEmail).thenReturn(
Rxn(PresentationEmail(id: EmailId(Id('1')))),
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess([]),
);
when(threadDetailController.emailIdsPresentation).thenReturn(
<EmailId, PresentationEmail?>{}.obs,
);
when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(success);
// assert
expect(
threadDetailController.emailIdsPresentation.keys,
[mailboxDashBoardController.selectedEmail.value!.id!],
threadDetailController.emailIdsPresentation,
initialEmailIdsPresentation,
);
});
test(
'should remove email id from emailIdsPresentation '
'which is not in GetThreadByIdSuccess.success.emailIds '
'and add new email ids from GetThreadByIdSuccess.success.emailIds '
'when GetThreadByIdSuccess.success.emailIds is not empty '
'and updateCurrentThreadDetail is true',
() {
// arrange
final initialEmailIdsPresentation = {
EmailId(Id('1')): PresentationEmail(),
EmailId(Id('2')): null,
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess(
[
EmailId(Id('1')),
EmailId(Id('3')),
EmailId(Id('4')),
],
updateCurrentThreadDetail: true,
),
);
// assert
expect(
threadDetailController.emailIdsPresentation,
{
EmailId(Id('1')): PresentationEmail(),
EmailId(Id('3')): null,
EmailId(Id('4')): null,
},
);
});
test(
'should add all email ids from GetThreadByIdSuccess.success.emailIds '
'except selected email id '
'and keep selected email id in emailIdsPresentation '
'when GetThreadByIdSuccess.success.emailIds is not empty '
'and updateCurrentThreadDetail is false '
'and selected email is available in emailIdsPresentation',
() {
// arrange
final existedSelectedEmailId = PresentationEmail(
id: EmailId(Id('1')),
emailInThreadStatus: EmailInThreadStatus.expanded,
);
final initialEmailIdsPresentation = <EmailId, PresentationEmail?>{
EmailId(Id('1')): existedSelectedEmailId,
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1')))));
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess(
[
EmailId(Id('1')),
EmailId(Id('3')),
EmailId(Id('4')),
],
updateCurrentThreadDetail: false,
),
);
// assert
expect(
threadDetailController.emailIdsPresentation,
{
EmailId(Id('1')): existedSelectedEmailId,
EmailId(Id('3')): null,
EmailId(Id('4')): null,
},
);
});
test(
'should add all email ids from GetThreadByIdSuccess.success.emailIds '
'except selected email id '
'and take selected email from mailbox dashboard controller '
'when GetThreadByIdSuccess.success.emailIds is not empty '
'and updateCurrentThreadDetail is false '
'and selected email is not available in emailIdsPresentation',
() {
// arrange
final selectedEmailId = PresentationEmail(id: EmailId(Id('1')));
final initialEmailIdsPresentation = <EmailId, PresentationEmail?>{
EmailId(Id('1')): null,
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1')))));
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess(
[
EmailId(Id('1')),
EmailId(Id('3')),
EmailId(Id('4')),
],
updateCurrentThreadDetail: false,
),
);
// assert
expect(
threadDetailController.emailIdsPresentation,
{
EmailId(Id('1')): selectedEmailId,
EmailId(Id('3')): null,
EmailId(Id('4')): null,
},
);
});
});
@@ -0,0 +1,90 @@
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'handle_get_thread_by_id_failure_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ThreadDetailController>()])
@GenerateNiceMocks([MockSpec<MailboxDashBoardController>()])
void main() {
late MockThreadDetailController controller;
late MockMailboxDashBoardController mailboxDashBoardController;
setUp(() {
controller = MockThreadDetailController();
mailboxDashBoardController = MockMailboxDashBoardController();
});
group('handle get thread by id failure test', () {
test(
'should not call consumeState '
'when updateCurrentThreadDetail is true',
() {
// arrange
final failure = GetThreadByIdFailure(updateCurrentThreadDetail: true);
// act
controller.handleGetThreadByIdFailure(failure);
// assert
verifyNever(controller.consumeState(any));
});
test(
'should consumeState GetEmailsByIdsSuccess with selected email '
'when updateCurrentThreadDetail is false '
'and selected email is not null',
() {
// arrange
final failure = GetThreadByIdFailure(updateCurrentThreadDetail: false);
final selectedEmail = PresentationEmail(id: EmailId(Id('1')));
when(controller.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(selectedEmail));
// act
controller.handleGetThreadByIdFailure(failure);
// assert
final streamsConsumed = (verify(
controller.consumeState(captureAny),
).captured as List<Object?>).first as Stream?;
expect(
streamsConsumed,
emitsInOrder([
Right(GetEmailsByIdsSuccess([selectedEmail])),
]),
);
});
test(
'should show retry toast '
'when updateCurrentThreadDetail is false '
'and selected email is null',
() {
// arrange
final failure = GetThreadByIdFailure(updateCurrentThreadDetail: false);
when(controller.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());
// act
controller.handleGetThreadByIdFailure(failure);
// assert
verify(controller.showRetryToast(failure));
verifyNever(controller.consumeState(any));
});
});
}
@@ -1,24 +1,20 @@
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import '../../../../fixtures/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart';
import 'handle_refresh_thread_detail_action_test.mocks.dart';
@GenerateNiceMocks([
@@ -31,9 +27,6 @@ void main() {
late MockMailboxDashBoardController mailboxDashBoardController;
late MockGetThreadByIdInteractor getThreadByIdInteractor;
final sentMailboxId = MailboxId(Id('sent'));
final ownEmailAddress = SessionFixtures.aliceSession.getOwnEmailAddress();
setUp(() {
threadDetailController = MockThreadDetailController();
mailboxDashBoardController = MockMailboxDashBoardController();
@@ -42,275 +35,93 @@ void main() {
group('handle refresh thread detail action test', () {
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email created contains currentThreadId '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
created: [Email(id: emailId, threadId: threadId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
'Should consume GetThreadByIdSuccess when isThreadDetailEnabled is false',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [Email(id: emailId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(false);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
final capturedStates = verify(
threadDetailController.consumeState(captureAny)
).captured;
expect(capturedStates, hasLength(1));
final stateStream = capturedStates.first as Stream;
final state = await stateStream.last;
expect(state, isA<Right>());
expect((state as Right).value, isA<GetThreadByIdSuccess>());
},
);
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email updated contains currentThreadId '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [Email(id: emailId, threadId: threadId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
'Should consume both success states when isThreadDetailEnabled is true with mixed email changes',
() async {
// arrange
final currentThreadId = ThreadId(Id('current-thread'));
final createdEmailId = EmailId(Id('created-email'));
final updatedEmailId = EmailId(Id('updated-email'));
final destroyedEmailId = EmailId(Id('destroyed-email'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
created: [Email(id: createdEmailId, threadId: currentThreadId)],
updated: [Email(id: updatedEmailId, threadId: currentThreadId)],
destroyed: [destroyedEmailId],
));
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email destroyed contains any of emailId in current thread '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
destroyed: [emailId],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.emailIdsPresentation).thenReturn({
emailId: null
}.obs);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(threadId: currentThreadId)));
when(threadDetailController.emailIdsPresentation).thenReturn(RxMap<EmailId, PresentationEmail?>.of({
destroyedEmailId: PresentationEmail(id: destroyedEmailId),
updatedEmailId: PresentationEmail(id: updatedEmailId),
}));
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
when(threadDetailController.currentExpandedEmailId).thenReturn(Rxn());
test(
'should consume GetThreadByIdSuccess with selected email id '
'when refreshThreadDetailAction is called '
'and isThreadDetailEnabled is false '
'and selected email id not null '
'and list email updated contains selected email id',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [Email(id: emailId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(false);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
final args = verify(
threadDetailController.consumeState(captureAny),
).captured.first as Stream;
final state = await args.last;
expect(
state,
Right(GetThreadByIdSuccess(
[emailId],
updateCurrentThreadDetail: true,
)),
);
});
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
test(
'should not call getThreadByIdInteractor.execute '
'and not consume GetThreadByIdSuccess with selected email id'
'when refreshThreadDetailAction is called '
'and list email created does not contain currentThreadId '
'and list email updated does not contain currentThreadId '
'and list email destroyed does not contain any of emailId in current thread '
'and isThreadDetailEnabled is true',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
created: [
Email(id: EmailId(Id('2')), threadId: ThreadId(Id('some-id-2'))),
],
updated: [
Email(id: EmailId(Id('3')), threadId: ThreadId(Id('some-id-3'))),
],
destroyed: [
EmailId(Id('4')),
],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail()));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.emailIdsPresentation).thenReturn({
emailId: null,
}.obs);
when(threadDetailController.viewState).thenReturn(Rx(Right(UIState.idle)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
verifyNever(threadDetailController.consumeState(any));
});
test(
'should not call getThreadByIdInteractor.execute '
'and not consume GetThreadByIdSuccess with selected email id'
'when refreshThreadDetailAction is called '
'and isThreadDetailEnabled is false '
'and updated email ids does not contain selected email id',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [
Email(id: EmailId(Id('3')), threadId: ThreadId(Id('some-id-3'))),
],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(false);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
verifyNever(threadDetailController.consumeState(any));
});
// assert
final capturedStates = verify(
threadDetailController.consumeState(captureAny)
).captured;
expect(capturedStates, hasLength(2));
// Verify GetThreadByIdSuccess
final firstState = await (capturedStates.first as Stream).last;
expect(firstState, isA<Right>());
final threadSuccess = (firstState as Right).value as GetThreadByIdSuccess;
expect(threadSuccess.emailIds, containsAll([createdEmailId, updatedEmailId]));
// Verify GetEmailsByIdsSuccess
final secondState = await (capturedStates[1] as Stream).last;
expect(secondState, isA<Right>());
final emailsSuccess = (secondState as Right).value as GetEmailsByIdsSuccess;
expect(emailsSuccess.presentationEmails, hasLength(2));
expect(emailsSuccess.presentationEmails.map((e) => e.id), containsAll([
createdEmailId,
updatedEmailId,
]));
},
);
});
}
}
@@ -33,7 +33,7 @@ void main() {
mailboxDashboardController = MockMailboxDashBoardController();
});
test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 3 elements', () async {
test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 2 elements', () async {
// Arrange
final emailIds = [
EmailId(Id('email1')),
@@ -63,7 +63,7 @@ void main() {
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashboardController);
when(mailboxDashboardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail()));
.thenReturn(Rxn(PresentationEmail(id: emailIds[0])));
// Act
threadDetailController.initializeThreadDetailEmails(
@@ -78,7 +78,7 @@ void main() {
properties: captureAnyNamed('properties'),
)).captured;
expect((captured[2] as List<EmailId>).length, 3);
expect((captured[2] as List<EmailId>).length, 2);
});
});
}
@@ -0,0 +1,94 @@
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import '../../../../fixtures/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart';
import 'thread_detail_on_selected_email_updated_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<ThreadDetailController>(),
MockSpec<GetThreadByIdInteractor>(),
MockSpec<MailboxDashBoardController>(),
])
void main() {
late MockThreadDetailController threadDetailController;
late MockGetThreadByIdInteractor getThreadByIdInteractor;
late MockMailboxDashBoardController mailboxDashboardController;
setUp(() {
threadDetailController = MockThreadDetailController();
getThreadByIdInteractor = MockGetThreadByIdInteractor();
mailboxDashboardController = MockMailboxDashBoardController();
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.additionalProperties)
.thenReturn(Properties.empty());
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashboardController);
});
group('thread detail on selected email updated test:', () {
test(
'should reset thread detail controller '
'when selected email is null',
() {
// act
threadDetailController.onSelectedEmailUpdated(
null,
getThreadByIdInteractor,
null,
);
// assert
verify(threadDetailController.reset()).called(1);
});
test(
'should not reset thread detail controller '
'and consume PreloadEmailIdsInThreadSuccess and PreloadEmailsByIdsSuccess '
'when selected email and its id is not null',
() async {
// arrange
final selectedEmail = PresentationEmail(
id: EmailId(Id('1')),
threadId: ThreadId(Id('1')),
);
when(threadDetailController.currentExpandedEmailId).thenReturn(Rxn());
// act
threadDetailController.onSelectedEmailUpdated(
selectedEmail,
getThreadByIdInteractor,
null,
);
// assert
verifyNever(threadDetailController.reset());
final streamsConsumed = (verify(
threadDetailController.consumeState(captureAny),
).captured as List<Object?>).first as Stream?;
expect(
streamsConsumed,
emitsInOrder([
Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!])),
Right(PreloadEmailsByIdsSuccess([selectedEmail])),
]),
);
});
});
}
@@ -1,6 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
void main() {
@@ -322,5 +325,172 @@ void main() {
);
});
});
group('refreshEmailIds', () {
test('should return original emails when no created or destroyed', () {
final original = [EmailId(Id('1')), EmailId(Id('2'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: [],
destroyed: [],
),
original,
);
});
test('should remove destroyed emails from original', () {
final original = [EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('3'))];
final destroyed = [EmailId(Id('2'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: [],
destroyed: destroyed,
),
[EmailId(Id('1')), EmailId(Id('3'))],
);
});
test('should add created emails to original', () {
final original = [EmailId(Id('1'))];
final created = [EmailId(Id('2')), EmailId(Id('3'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: created,
destroyed: [],
),
[EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('3'))],
);
});
test('should combine created and destroyed operations', () {
final original = [EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('3'))];
final created = [EmailId(Id('4'))];
final destroyed = [EmailId(Id('2'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: created,
destroyed: destroyed,
),
[EmailId(Id('1')), EmailId(Id('3')), EmailId(Id('4'))],
);
});
test('should ignore destroyed emails not present in original', () {
final original = [EmailId(Id('1'))];
final destroyed = [EmailId(Id('99'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: [],
destroyed: destroyed,
),
original,
);
});
test('should handle duplicate created emails', () {
final original = [EmailId(Id('1'))];
final created = [EmailId(Id('2')), EmailId(Id('2'))];
expect(
ThreadDetailPresentationUtils.refreshEmailIds(
original: original,
created: created,
destroyed: [],
),
[EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('2'))],
);
});
});
group('refreshPresentationEmails', () {
final email1 = PresentationEmail(id: EmailId(Id('1')), keywords: {}, mailboxIds: {});
final email2 = PresentationEmail(id: EmailId(Id('2')), keywords: {}, mailboxIds: {});
final email3 = PresentationEmail(id: EmailId(Id('3')), keywords: {}, mailboxIds: {});
final updatedEmail2 = PresentationEmail(
id: EmailId(Id('2')),
keywords: {KeyWordIdentifier('updated'): true},
mailboxIds: {MailboxId(Id('mailbox')): true}
);
test('should combine non-destroyed originals with updates and created emails', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [email1, email2],
created: [email3],
updated: [updatedEmail2],
destroyed: [EmailId(Id('1'))],
);
expect(result, [
email2.copyWith(
keywords: updatedEmail2.keywords,
mailboxIds: updatedEmail2.mailboxIds,
),
email3,
]);
});
test('should handle empty destroyed and updated lists', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [email1],
created: [email2],
updated: [],
destroyed: [],
);
expect(result, [email1, email2]);
});
test('should exclude all destroyed emails', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [email1, email2],
created: [email3],
updated: [],
destroyed: [EmailId(Id('1')), EmailId(Id('2'))],
);
expect(result, [email3]);
});
test('should apply updates to remaining original emails', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [email1, email2],
created: [],
updated: [updatedEmail2],
destroyed: [],
);
expect(result, [
email1,
updatedEmail2,
]);
});
test('should handle empty original list', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [],
created: [email1, email2],
updated: [],
destroyed: [],
);
expect(result, [email1, email2]);
});
test('should handle emails being both destroyed and updated', () {
final result = ThreadDetailPresentationUtils.refreshPresentationEmails(
original: [email1],
created: [],
updated: [email1.copyWith(keywords: {KeyWordIdentifier('test'): true})],
destroyed: [EmailId(Id('1'))],
);
expect(result, isEmpty);
});
});
});
}