From 1c916b4cf1b60ea4f1a6d98ac6884080fd0cb94a Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 25 Mar 2024 15:17:26 +0700 Subject: [PATCH] TF-2717 Write widgets test for RecipientComposerWidget --- .../composer/presentation/composer_view.dart | 14 +- .../presentation/composer_view_web.dart | 18 + .../view/mobile/mobile_container_view.dart | 4 +- .../widgets/recipient_composer_widget.dart | 486 +++++++++--------- .../recipient_suggestion_item_widget.dart | 9 +- .../widgets/recipient_tag_item_widget.dart | 26 +- .../recipient_composer_widget_test.dart | 184 +++++++ 7 files changed, 487 insertions(+), 254 deletions(-) create mode 100644 test/features/features/composer/recipient_composer_widget_test.dart diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index c42f3b650..765f9c5f0 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -49,7 +49,7 @@ class ComposerView extends GetWidget { ? controller.insertImage(context, constraints.maxWidth) : null, backgroundColor: MobileAppBarComposerWidgetStyle.backgroundColor, - childBuilder: (context) => SafeArea( + childBuilder: (context, constraints) => SafeArea( left: !controller.responsiveUtils.isLandscapeMobile(context), right: !controller.responsiveUtils.isLandscapeMobile(context), child: Container( @@ -110,6 +110,8 @@ class ComposerView extends GetWidget { Obx(() => RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -134,6 +136,8 @@ class ComposerView extends GetWidget { return RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -158,6 +162,8 @@ class ComposerView extends GetWidget { return RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -264,6 +270,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -287,6 +295,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -306,6 +316,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 8a49da6b6..e2d353148 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -87,6 +87,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -110,6 +112,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -130,6 +134,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -305,6 +311,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -328,6 +336,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -348,6 +358,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -562,6 +574,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -585,6 +599,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -605,6 +621,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart index 6854dbeec..2c6c23f27 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -12,7 +12,7 @@ typedef OnInsertImageAction = Function(BoxConstraints constraints); class MobileContainerView extends StatelessWidget { - final Widget Function(BuildContext context) childBuilder; + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; final rich_composer.RichTextController keyboardRichTextController; final VoidCallback onCloseViewAction; final VoidCallback? onAttachFileAction; @@ -69,7 +69,7 @@ class MobileContainerView extends StatelessWidget { paddingChild: isKeyboardVisible ? MobileContainerViewStyle.keyboardToolbarPadding : EdgeInsets.zero, - child: childBuilder(context), + child: childBuilder(context, constraints), ); }); }) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index d2427489e..c94831593 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -11,7 +11,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -41,6 +40,8 @@ class RecipientComposerWidget extends StatefulWidget { final PrefixEmailAddress prefix; final List listEmailAddress; + final ImagePaths imagePaths; + final double maxWidth; final ExpandMode expandMode; final PrefixRecipientState fromState; final PrefixRecipientState ccState; @@ -66,6 +67,8 @@ class RecipientComposerWidget extends StatefulWidget { super.key, required this.prefix, required this.listEmailAddress, + required this.imagePaths, + required this.maxWidth, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, @@ -99,8 +102,6 @@ class _RecipientComposerWidgetState extends State { bool _isDragging = false; late List _currentListEmailAddress; - final _imagePaths = Get.find(); - @override void initState() { super.initState(); @@ -117,250 +118,255 @@ class _RecipientComposerWidgetState extends State { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: RecipientComposerWidgetStyle.borderColor, - width: 1 - ) + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1 ) - ), - padding: widget.padding, - margin: widget.margin, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: RecipientComposerWidgetStyle.labelMargin, - child: Text( - '${widget.prefix.asName(context)}:', - style: RecipientComposerWidgetStyle.labelTextStyle - ), + ) + ), + padding: widget.padding, + margin: widget.margin, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: RecipientComposerWidgetStyle.labelMargin, + child: Text( + '${widget.prefix.asName(context)}:', + key: Key('prefix_${widget.prefix.name}_recipient_composer_widget'), + style: RecipientComposerWidgetStyle.labelTextStyle ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - Expanded( - child: FocusScope( - child: Focus( - onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), - onKey: (focusNode, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - widget.nextFocusNode?.requestFocus(); - widget.onFocusNextAddressAction?.call(); - return KeyEventResult.handled; + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + Expanded( + child: FocusScope( + child: Focus( + onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), + onKey: (focusNode, event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + widget.nextFocusNode?.requestFocus(); + widget.onFocusNextAddressAction?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: StatefulBuilder( + builder: (context, stateSetter) { + if (PlatformInfo.isWeb) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + }, + onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), + onLeave: (draggableEmailAddress) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + stateSetter(() => _isDragging = true); + } + }, + ); + } else { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); } - return KeyEventResult.ignored; }, - child: StatefulBuilder( - builder: (context, stateSetter) { - if (PlatformInfo.isWeb) { - return DragTarget( - builder: (context, candidateData, rejectedData) { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - enableBorder: _isDragging, - borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, - enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - }, - onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), - onLeave: (draggableEmailAddress) { - if (_isDragging) { - stateSetter(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - stateSetter(() => _isDragging = true); - } - }, - ); - } else { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - } - }, - ) ) ) - ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to) - if (PlatformInfo.isWeb) - ...[ - if (widget.fromState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).from_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), - ), - if (widget.ccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).cc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), - ), - if (widget.bccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).bcc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), - ), - ] - else if (PlatformInfo.isMobile) - ...[ - TMailButtonWidget.fromIcon( - icon: _isAllRecipientInputEnabled - ? _imagePaths.icChevronUp - : _imagePaths.icChevronDownOutline, + ) + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + if (widget.prefix == PrefixEmailAddress.to) + if (PlatformInfo.isWeb) + ...[ + if (widget.fromState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).from_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, backgroundColor: Colors.transparent, - iconSize: 24, - padding: const EdgeInsets.all(5), - iconColor: AppColor.colorLabelComposer, - margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, - onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), - ) - ] - else if (PlatformInfo.isWeb) - TMailButtonWidget.fromIcon( - icon: _imagePaths.icClose, - backgroundColor: Colors.transparent, - iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, - iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, - padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), - ) - ] - ), - ); - }); + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), + ), + if (widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + ] + else if (PlatformInfo.isMobile) + ...[ + TMailButtonWidget.fromIcon( + icon: _isAllRecipientInputEnabled + ? widget.imagePaths.icChevronUp + : widget.imagePaths.icChevronDownOutline, + backgroundColor: Colors.transparent, + iconSize: 24, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + ) + ] + else if (PlatformInfo.isWeb) + TMailButtonWidget.fromIcon( + icon: widget.imagePaths.icClose, + backgroundColor: Colors.transparent, + iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, + iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, + padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), + ) + ] + ), + ); } bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart index 87b83b29c..29dc018b0 100644 --- a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -3,7 +3,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:super_tag_editor/widgets/rich_text_widget.dart'; @@ -17,16 +16,16 @@ class RecipientSuggestionItemWidget extends StatelessWidget { final SuggestionEmailState suggestionState; final EmailAddress emailAddress; + final ImagePaths imagePaths; final String? suggestionValid; final bool highlight; final OnSelectedRecipientSuggestionAction? onSelectedAction; - final _imagePaths = Get.find(); - - RecipientSuggestionItemWidget({ + const RecipientSuggestionItemWidget({ super.key, required this.suggestionState, required this.emailAddress, + required this.imagePaths, this.suggestionValid, this.highlight = false, this.onSelectedAction, @@ -61,7 +60,7 @@ class RecipientSuggestionItemWidget extends StatelessWidget { ) : null, trailing: SvgPicture.asset( - _imagePaths.icFilterSelected, + imagePaths.icFilterSelected, width: RecipientSuggestionItemWidgetStyle.selectedIconSize, height: RecipientSuggestionItemWidgetStyle.selectedIconSize, fit: BoxFit.fill diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index 57b981ed9..d7867b281 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -8,7 +8,6 @@ import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -23,7 +22,9 @@ class RecipientTagItemWidget extends StatelessWidget { final bool isCollapsed; final bool isLatestTagFocused; final bool isLatestEmail; + final ImagePaths imagePaths; final double? maxWidth; + final int index; final PrefixEmailAddress prefix; final EmailAddress currentEmailAddress; final List currentListEmailAddress; @@ -31,14 +32,14 @@ class RecipientTagItemWidget extends StatelessWidget { final OnShowFullListEmailAddressAction? onShowFullAction; final OnDeleteTagAction? onDeleteTagAction; - final _imagePaths = Get.find(); - - RecipientTagItemWidget({ + const RecipientTagItemWidget({ super.key, + required this.index, required this.prefix, required this.currentEmailAddress, required this.currentListEmailAddress, required this.collapsedListEmailAddress, + required this.imagePaths, this.isCollapsed = false, this.isLatestTagFocused = false, this.isLatestEmail = false, @@ -50,6 +51,7 @@ class RecipientTagItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + key: Key('recipient_tag_item_${prefix.name}_$index'), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Row( mainAxisSize: MainAxisSize.min, @@ -77,12 +79,17 @@ class RecipientTagItemWidget extends StatelessWidget { ), padding: EdgeInsets.zero, label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), currentEmailAddress.asString(), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, ), - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, backgroundColor: _getTagBackgroundColor(), side: _getTagBorderSide(), @@ -91,6 +98,7 @@ class RecipientTagItemWidget extends StatelessWidget { ), avatar: currentEmailAddress.displayName.isNotEmpty ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), colors: currentEmailAddress.avatarColors, label: currentEmailAddress.displayName.firstLetterToUpperCase, labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, @@ -116,13 +124,18 @@ class RecipientTagItemWidget extends StatelessWidget { vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 ), label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), currentEmailAddress.asString(), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, ), padding: EdgeInsets.zero, - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, backgroundColor: _getTagBackgroundColor(), side: _getTagBorderSide(), @@ -131,6 +144,7 @@ class RecipientTagItemWidget extends StatelessWidget { ), avatar: currentEmailAddress.displayName.isNotEmpty ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), colors: currentEmailAddress.avatarColors, label: currentEmailAddress.displayName.firstLetterToUpperCase, labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart new file mode 100644 index 000000000..b17b3b992 --- /dev/null +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -0,0 +1,184 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:super_tag_editor/tag_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; + +void main() { + group('recipient_composer_widget test', () { + final imagePaths = ImagePaths(); + final keyEmailTagEditor = GlobalKey(); + const prefix = PrefixEmailAddress.to; + + Widget makeTestableWidget({required Widget child}) { + return GetMaterialApp( + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + home: Scaffold(body: child), + ); + } + + testWidgets('RecipientComposerWidget renders correctly', (tester) async { + final listEmailAddress = []; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientComposerWidgetFinder = find.byType(RecipientComposerWidget); + + expect(recipientComposerWidgetFinder, findsOneWidget); + }); + + testWidgets('RecipientComposerWidget renders list email address correctly', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test1@example.com'), + EmailAddress(null, 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect(find.byType(RecipientTagItemWidget), findsNWidgets(2)); + expect(find.text('test1@example.com'), findsOneWidget); + expect(find.text('test2@example.com'), findsOneWidget); + }); + + testWidgets('RecipientTagItemWidget should have a `maxWidth` equal to the RecipientComposerWidget\'s `maxWidth`', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Container recipientTagItemWidget = tester.widget(recipientTagItemWidgetFinder); + + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect(recipientTagItemWidget.constraints!.maxWidth, 360); + }); + + testWidgets('RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, + lessThanOrEqualTo(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('RecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect( + prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, + lessThanOrEqualTo(360) + ); + }); + }); +}