TF-3912 Thread Detail Fix real time update

This commit is contained in:
DatDang
2025-07-29 16:43:03 +07:00
committed by Dat H. Pham
parent 4f0c783ea7
commit c7f5dc9e5e
12 changed files with 262 additions and 7 deletions
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import '../base/core_robot.dart';
@@ -8,4 +10,25 @@ class SettingRobot extends CoreRobot {
Future<void> openLanguageMenuItem() async {
await $(#setting_language_region).tap();
}
Future<void> openPreferencesMenuItem() async {
await $(#setting_preferences).tap();
}
Future<void> switchOnThreadSetting() async {
final threadSettingOn = $(ValueKey(AppLocalizations().thread))
.$(#setting_option_switch_on)
.visible;
if (!threadSettingOn) {
await $(ValueKey(AppLocalizations().thread)).tap();
}
}
Future<void> backToSettingsFromFirstLevel() async {
await $(#settings_first_level_close_button).tap();
}
Future<void> closeSettings() async {
await $(#settings_close_button).tap();
}
}
@@ -0,0 +1,53 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart';
import '../../base/base_test_scenario.dart';
import '../../robots/composer_robot.dart';
import '../../robots/email_robot.dart';
import '../../robots/mailbox_menu_robot.dart';
import '../../robots/search_robot.dart';
import '../../robots/setting_robot.dart';
import '../../robots/thread_robot.dart';
class ThreadDetailReplyRealTimeUpdateScenario extends BaseTestScenario {
ThreadDetailReplyRealTimeUpdateScenario(super.$);
@override
Future<void> runTestLogic() async {
const subject = 'Reply thread';
const replyContent = 'reply thread detail';
final threadRobot = ThreadRobot($);
final mailboxMenuRobot = MailboxMenuRobot($);
final settingRobot = SettingRobot($);
final searchRobot = SearchRobot($);
final emailRobot = EmailRobot($);
final composerRobot = ComposerRobot($);
final imagePaths = ImagePaths();
await threadRobot.openMailbox();
await mailboxMenuRobot.openSetting();
await settingRobot.openPreferencesMenuItem();
await settingRobot.switchOnThreadSetting();
await settingRobot.backToSettingsFromFirstLevel();
await settingRobot.closeSettings();
await threadRobot.openSearchView();
await searchRobot.enterQueryString(subject);
await searchRobot.tapOnShowAllResultsText();
await searchRobot.openEmailWithSubject(subject);
await emailRobot.onTapReplyEmail();
await composerRobot.grantContactPermission();
await composerRobot.addContent(replyContent);
await composerRobot.sendEmail(imagePaths);
await $.waitUntilVisible($(ThreadDetailCollapsedEmail));
expect(
$(ThreadDetailCollapsedEmail).which<ThreadDetailCollapsedEmail>((widget) {
return widget.preview.contains(replyContent);
}),
findsOneWidget,
);
}
}
@@ -0,0 +1,9 @@
import '../../base/test_base.dart';
import '../../scenarios/thread_detail/thread_detail_reply_real_time_update_scenario.dart';
void main() {
TestBase().runPatrolTest(
description: 'Should see reply of thread detail',
scenarioBuilder: ($) => ThreadDetailReplyRealTimeUpdateScenario($),
);
}
@@ -144,6 +144,8 @@ class GetEmailContentInteractor {
emailCurrent: emailCache.copyWith(
headers: detailedEmail.headers,
sMimeStatusHeader: detailedEmail.sMimeStatusHeader,
references: detailedEmail.references,
messageId: detailedEmail.messageId,
)
));
} catch (e) {
@@ -183,6 +185,8 @@ class GetEmailContentInteractor {
headers: detailedEmail.headers,
sMimeStatusHeader: detailedEmail.sMimeStatusHeader,
identityHeader: detailedEmail.identityHeader,
references: detailedEmail.references,
messageId: detailedEmail.messageId,
)
));
} catch (e) {
@@ -33,6 +33,7 @@ class SettingFirstLevelAppBar extends StatelessWidget {
PositionedDirectional(
start: 0,
child: TMailButtonWidget.fromIcon(
key: const ValueKey('settings_first_level_close_button'),
icon: imagePaths.icArrowBack,
tooltipMessage: appLocalizations.back,
backgroundColor: Colors.transparent,
@@ -72,6 +72,7 @@ class SettingsFirstLevelView extends GetWidget<SettingsController> {
if (controller.manageAccountDashboardController.isServerSettingsCapabilitySupported) {
return Column(children: [
SettingFirstLevelTileBuilder(
key: const ValueKey('setting_preferences'),
AccountMenuItem.preferences.getName(AppLocalizations.of(context)),
AccountMenuItem.preferences.getIcon(controller.imagePaths),
subtitle: AppLocalizations.of(context).emailReadReceiptsSettingExplanation,
@@ -37,6 +37,7 @@ class UniversalSettingAppBar extends StatelessWidget {
PositionedDirectional(
start: 24,
child: TMailButtonWidget.fromIcon(
key: const ValueKey('settings_close_button'),
icon: imagePaths.icClose,
iconSize: 28,
padding: const EdgeInsets.all(5),
@@ -59,11 +59,17 @@ class SettingOptionItem extends StatelessWidget {
Row(
children: [
InkWell(
key: ValueKey(optionType.getTitle(appLocalizations)),
onTap: () => onTapSettingOptionAction(
optionType,
optionType.isEnabled(settingOption, localSettings),
),
child: SvgPicture.asset(
key: ValueKey(
optionType.isEnabled(settingOption, localSettings)
? 'setting_option_switch_on'
: 'setting_option_switch_off',
),
optionType.isEnabled(settingOption, localSettings)
? imagePaths.icSwitchOn
: imagePaths.icSwitchOff,
@@ -104,11 +104,14 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
Email email,
ThreadId currentThreadId,
) {
return email.threadId == currentThreadId &&
sentMailboxId != null &&
!email.inSentMailbox(sentMailboxId!) &&
ownEmailAddress != null &&
!email.fromMe(ownEmailAddress!) &&
!email.recipientsHasMe(ownEmailAddress!);
if (email.threadId != currentThreadId) return false;
if (sentMailboxId != null && !email.inSentMailbox(sentMailboxId!)) {
return true;
}
return ownEmailAddress != null &&
(!email.fromMe(ownEmailAddress!) ||
!email.recipientsHasMe(ownEmailAddress!));
}
}
@@ -0,0 +1,26 @@
Return-Path: <emma@example.com>
MIME-Version: 1.0
References: <Mime4j.4c.b3b452d18f0d0ef9.192d67aaafc@example.com>
Subject: Reply thread
From: emma@example.com
To: "bob" <bob@example.com>
Cc: "alice" <alice@example.com>
Bcc: "brian" <brian@example.com>
Reply-To: emma-reply-to@example.com
Date: Tue, 17 Dec 2024 16:31:00 +0000
Message-ID: <Mime4j.4e.728b2b72b23cc182.192d67ae03c@example.com>
User-Agent: Twake-Mail/0.13.2 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15;
rv:131.0) Gecko/20100101 Firefox/131.0
Content-Type: multipart/alternative;
boundary="-=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=-"
List-Post: <mailto:emma-reply-to-list@example.com>
---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=-
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Accept-Language: fr-FR, en-US, vi-VN, ru-RU, ar-TN, it-IT
Content-Language: en-US
Reply thread
---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=---
@@ -70,7 +70,7 @@ james-cli ImportEml \#private "bob@example.com" "Calendar" "/root/conf/integrati
# For test reply email
# Import emails into 'Reply Emails' folder for user Bob
replyEmailsEML=("reply-all.eml" "reply-to-list.eml" "with-reply-to.eml" "without-reply-to.eml")
replyEmailsEML=("reply-all.eml" "reply-to-list.eml" "with-reply-to.eml" "without-reply-to.eml" "reply-thread.eml")
for eml in "${replyEmailsEML[@]}"; do
echo "Importing $eml into 'Reply Emails' folder for user bob"
@@ -3,6 +3,7 @@ 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/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@@ -129,4 +130,131 @@ void main() {
},
);
});
group('validateNewCreatedEmailForCurrentThread test:', () {
test(
'should return false '
'when email is not in same thread as current thread',
() {
// arrange
final email = Email(threadId: ThreadId(Id('other-thread-id')));
final threadId = ThreadId(Id('thread-id'));
// act
final result = threadDetailController.validateNewCreatedEmailForCurrentThread(
email,
threadId,
);
// assert
expect(result, false);
});
test(
'should return true '
'when email is in same thread as current thread '
'and email is not in sent mailbox',
() {
// arrange
final threadId = ThreadId(Id('thread-id'));
final sentMailboxId = MailboxId(Id('sent-mailbox-id'));
final email = Email(threadId: threadId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
// act
final result = threadDetailController.validateNewCreatedEmailForCurrentThread(
email,
threadId,
);
// assert
expect(result, true);
});
test(
'should return true '
'when email is in same thread as current thread '
'and email is in sent mailbox '
'and email is not sent by current user',
() {
// arrange
final threadId = ThreadId(Id('thread-id'));
final sentMailboxId = MailboxId(Id('sent-mailbox-id'));
final email = Email(
threadId: threadId,
mailboxIds: {sentMailboxId: true},
);
const ownEmailAddress = '9jEYK@example.com';
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
// act
final result = threadDetailController.validateNewCreatedEmailForCurrentThread(
email,
threadId,
);
// assert
expect(result, true);
});
test(
'should return true '
'when email is in same thread as current thread '
'and email is in sent mailbox '
'and email is sent by current user '
'and email is not sent to current user',
() {
// arrange
final threadId = ThreadId(Id('thread-id'));
final sentMailboxId = MailboxId(Id('sent-mailbox-id'));
const ownEmailAddress = '9jEYK@example.com';
final email = Email(
threadId: threadId,
mailboxIds: {sentMailboxId: true},
from: {EmailAddress(null, ownEmailAddress)},
);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
// act
final result = threadDetailController.validateNewCreatedEmailForCurrentThread(
email,
threadId,
);
// assert
expect(result, true);
});
test(
'should return false '
'when email is in same thread as current thread '
'and email is in sent mailbox '
'and email is sent by current user '
'and email is sent to current user',
() {
// arrange
final threadId = ThreadId(Id('thread-id'));
final sentMailboxId = MailboxId(Id('sent-mailbox-id'));
const ownEmailAddress = '9jEYK@example.com';
final email = Email(
threadId: threadId,
mailboxIds: {sentMailboxId: true},
from: {EmailAddress(null, ownEmailAddress)},
to: {EmailAddress(null, ownEmailAddress)},
);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
// act
final result = threadDetailController.validateNewCreatedEmailForCurrentThread(
email,
threadId,
);
// assert
expect(result, false);
});
});
}