Skip to content
6 changes: 6 additions & 0 deletions integration_test/base/base_test_scenario.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ abstract class BaseTestScenario extends BaseScenario

@override
Future<void> execute() async {
await setupPreLogin();
await executeLoginScenario();
await runTestLogic();
await disposeAfterTest();
}

Future<void> setupPreLogin() async {}

Future<void> runTestLogic();

Future<void> disposeAfterTest() async {}
}
90 changes: 90 additions & 0 deletions integration_test/mixin/provisioning_label_scenario_mixin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:core/utils/app_logger.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';

import '../models/provisioning_email.dart';
import '../models/provisioning_label.dart';

mixin ProvisioningLabelScenarioMixin {
Future<List<Label>> provisionLabels(
List<ProvisioningLabel> provisioningLabels,
) async {
if (provisioningLabels.isEmpty) {
return [];
}

final dashboardController = getBinding<MailboxDashBoardController>();
final createLabelInteractor = getBinding<CreateNewLabelInteractor>();

final accountId = dashboardController?.accountId.value;
final labelController = dashboardController?.labelController;

if (dashboardController == null ||
createLabelInteractor == null ||
accountId == null) {
log(
'ProvisioningLabelScenarioMixin::provisionLabels '
'skipped: missing dashboardController, CreateNewLabelInteractor, or accountId',
);
return [];
}

final List<Label?> results = await Future.wait(provisioningLabels.map(
(label) => _createLabel(createLabelInteractor, accountId, label),
));

final List<Label> createdLabels = results.whereType<Label>().toList();

if (createdLabels.isNotEmpty) {
labelController?.getAllLabels(accountId);
}

return createdLabels;
}

Future<Label?> _createLabel(
CreateNewLabelInteractor createLabelInteractor,
AccountId accountId,
ProvisioningLabel provisioningLabel,
) async {
final result = await createLabelInteractor
.execute(accountId, provisioningLabel.toLabel())
.last;

return result.fold(
(_) => null,
(success) => success is CreateNewLabelSuccess ? success.newLabel : null,
);
}

Future<List<Label>> provisionLabelsByDisplayNames(List<String> labelNames) {
return provisionLabels(
labelNames.map(ProvisioningLabel.new).toList(),
);
}

List<ProvisioningEmail> buildEmailsForLabel({
required Label label,
required String toEmail,
required int count,
}) {
return List.generate(
count,
(index) {
final emailNumber = index + 1;
final displayName = label.safeDisplayName;

return ProvisioningEmail(
toEmail: toEmail,
subject: 'Email $emailNumber subject $displayName',
content: 'Email $emailNumber content $displayName',
labels: [label],
);
},
);
}
}
2 changes: 2 additions & 0 deletions integration_test/mixin/scenario_utils_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:jmap_dart_client/jmap/jmap_request.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/email/set/set_email_method.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:labels/extensions/list_label_extension.dart';
import 'package:model/model.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart';
Expand Down Expand Up @@ -81,6 +82,7 @@ mixin ScenarioUtilsMixin {
identity: identity,
attachments: attachments,
hasRequestReadReceipt: requestReadReceipt,
keywords: provisioningEmail.labels.keywords,
),
)
.last;
Expand Down
4 changes: 4 additions & 0 deletions integration_test/models/provisioning_email.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import 'package:labels/labels.dart';

class ProvisioningEmail {
final String toEmail;
final String subject;
final String content;
final List<String> attachmentPaths;
final List<Label> labels;

ProvisioningEmail({
required this.toEmail,
required this.subject,
required this.content,
this.attachmentPaths = const [],
this.labels = const [],
});
}
9 changes: 9 additions & 0 deletions integration_test/models/provisioning_label.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:labels/model/label.dart';

class ProvisioningLabel {
final String displayName;

ProvisioningLabel(this.displayName);

Label toLabel() => Label(displayName: displayName);
}
14 changes: 14 additions & 0 deletions integration_test/robots/label_list_context_menu_robot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/features/base/widget/context_menu/context_menu_dialog_item.dart';

import '../base/core_robot.dart';

class LabelListContextMenuRobot extends CoreRobot {
LabelListContextMenuRobot(super.$);

Future<void> selectLabelByName(String name) async {
final item = $(ContextMenuDialogItem).$(name);
await $.scrollUntilVisible(finder: item);
await item.tap();
}
}
14 changes: 14 additions & 0 deletions integration_test/robots/label_robot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart';

import '../base/core_robot.dart';

class LabelRobot extends CoreRobot {
LabelRobot(super.$);

Future<void> openLabelByName(String name) async {
final item = $(LabelListItem).$(name);
await $.scrollUntilVisible(finder: item);
await item.tap();
}
}
4 changes: 4 additions & 0 deletions integration_test/robots/search_robot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,8 @@ class SearchRobot extends CoreRobot {
);
await $(#mobile_hasAttachment_search_filter_button).tap();
}

Future<void> openLabelListModal() async {
await $(#mobile_labels_search_filter_button).tap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart';

import '../../base/base_test_scenario.dart';
import '../../mixin/provisioning_label_scenario_mixin.dart';
import '../../robots/label_list_context_menu_robot.dart';
import '../../robots/search_robot.dart';
import '../../robots/thread_robot.dart';

class SearchEmailWithTagScenario extends BaseTestScenario
with ProvisioningLabelScenarioMixin {
const SearchEmailWithTagScenario(super.$);

@override
Future<void> setupPreLogin() async {
PlatformInfo.isIntegrationTesting = true;
}

@override
Future<void> runTestLogic() async {
const emailUser = String.fromEnvironment('BASIC_AUTH_EMAIL');

final threadRobot = ThreadRobot($);
final searchRobot = SearchRobot($);
final labelListContextMenuRobot = LabelListContextMenuRobot($);

final labels = await provisionLabelsByDisplayNames(
['Search Tag 1', 'Search Tag 2', 'Search Tag 3'],
);
await $.pumpAndSettle();

int emailCount = 3;
for (final label in labels) {
await provisionEmail(
buildEmailsForLabel(
label: label,
toEmail: emailUser,
count: emailCount,
),
requestReadReceipt: false,
);
}
await $.pumpAndSettle(duration: const Duration(seconds: 2));

await threadRobot.openSearchView();
await _expectSearchViewVisible();

for (final label in labels) {
final labelDisplayName = label.safeDisplayName;

await searchRobot.openLabelListModal();
await _expectLabelListContextMenuVisible();

await labelListContextMenuRobot.selectLabelByName(labelDisplayName);
await _expectEmailListDisplayedCorrectByTag(
tagDisplayName: labelDisplayName,
emailCount: emailCount,
);

await $.pumpAndSettle(duration: const Duration(seconds: 1));
}
}

Future<void> _expectSearchViewVisible() async {
await expectViewVisible($(SearchEmailView));
}

Future<void> _expectLabelListContextMenuVisible() async {
await expectViewVisible($(#label_list_bottom_sheet_context_menu));
}

Future<void> _expectEmailListDisplayedCorrectByTag({
required String tagDisplayName,
required int emailCount,
}) async {
// Emails provisioned by buildEmailsForLabel include the tag name in the subject
final listEmailTileWithTag = $.tester.widgetList<EmailTileBuilder>(
$(EmailTileBuilder).which<EmailTileBuilder>((widget) =>
widget.presentationEmail.subject?.contains(tagDisplayName) == true),
);

expect(listEmailTileWithTag.length, greaterThanOrEqualTo(emailCount));
}

@override
Future<void> disposeAfterTest() async {
PlatformInfo.isIntegrationTesting = false;
}
}
9 changes: 9 additions & 0 deletions integration_test/tests/search/search_email_with_tag_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import '../../base/test_base.dart';
import '../../scenarios/search/search_email_with_tag_scenario.dart';

void main() {
TestBase().runPatrolTest(
description: 'Should display email list correctly when search with tag',
scenarioBuilder: ($) => SearchEmailWithTagScenario($),
);
}
2 changes: 1 addition & 1 deletion labels/lib/extensions/label_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension LabelExtension on Label {
if (id?.value == moreLabelId) {
return AppColor.grayBackgroundColor;
} else {
return color?.value.toColor();
return color?.value.toColor() ?? AppColor.primaryMain;
}
}

Expand Down
4 changes: 4 additions & 0 deletions labels/lib/extensions/list_label_extension.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:labels/extensions/label_extension.dart';
import 'package:labels/model/label.dart';

Expand All @@ -13,4 +14,7 @@ extension ListLabelExtension on List<Label> {
List<String> get displayNameNotNullList => map((label) => label.safeDisplayName)
.where((name) => name.trim().isNotEmpty)
.toList();

List<KeyWordIdentifier> get keywords =>
map((label) => label.keyword).nonNulls.toList();
}
11 changes: 10 additions & 1 deletion lib/features/base/mixin/popup_context_menu_action_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mixin PopupContextMenuActionMixin {
required OnContextMenuActionClick onContextMenuActionClick,
Key? key,
bool useGroupedActions = false,
double? maxHeight,
}) async {
return await showModalBottomSheet(
context: context,
Expand All @@ -42,6 +43,9 @@ mixin PopupContextMenuActionMixin {
),
backgroundColor: Colors.white,
barrierColor: Colors.black.withValues(alpha: 0.2),
constraints: BoxConstraints(
maxHeight: maxHeight ?? double.infinity,
),
builder: (_) {
return PointerInterceptor(
child: Container(
Expand All @@ -63,6 +67,7 @@ mixin PopupContextMenuActionMixin {
BuildContext context,
RelativeRect position,
List<PopupMenuEntry> popupMenuItems,
{double? maxHeight}
) async {
return await showMenu(
context: context,
Expand All @@ -74,7 +79,11 @@ mixin PopupContextMenuActionMixin {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
constraints: const BoxConstraints(maxWidth: 300, minWidth: 178),
constraints: BoxConstraints(
maxWidth: 300,
minWidth: 178,
maxHeight: maxHeight ?? double.infinity,
),
items: popupMenuItems,
);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/features/base/model/filter_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum FilterField {
mailBox,
date,
sortBy,
labels,
hasAttachment,
deletionDate,
receptionDate,
Expand Down Expand Up @@ -49,6 +50,8 @@ enum FilterField {
return appLocalizations.headerRecipients;
case FilterField.sender:
return appLocalizations.sender;
case FilterField.labels:
return appLocalizations.labels;
default:
return '';
}
Expand Down Expand Up @@ -77,6 +80,8 @@ enum FilterField {
case FilterField.recipients:
case FilterField.sender:
return appLocalizations.addAnEmailAddress;
case FilterField.labels:
return appLocalizations.allLabels;
default:
return '';
}
Expand Down
2 changes: 2 additions & 0 deletions lib/features/base/model/popup_menu_item_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ mixin OptionalPopupSelectedIcon<T> {
Color get selectedIconColor => AppColor.primaryMain;

double get selectedIconSize => 16.0;

bool get isArrangeRTL => true;
}

mixin OptionalPopupHoverIcon {
Expand Down
Loading
Loading