From 0b18961211b4f03c66f365d229616c3942a1f93a Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Thu, 22 Aug 2024 13:43:56 +0100 Subject: [PATCH] fix(UX-1201): Update Slidable button colors in ZetaChatItem (#156) test: Added basic test cases for ZetaChatItem --- .../pages/components/chat_item_example.dart | 28 +- lib/src/components/chat_item/chat_item.dart | 92 +++- .../components/chat_item/chat_item_test.dart | 484 ++++++++++++++++++ .../golden/chat_item_custom_leading.png | Bin 0 -> 2486 bytes .../chat_item_custom_slidable_buttons.png | Bin 0 -> 3034 bytes .../chat_item/golden/chat_item_default.png | Bin 0 -> 3910 bytes .../golden/chat_item_highlighted.png | Bin 0 -> 4022 bytes .../chat_item_pale_and_regular_buttons.png | Bin 0 -> 2972 bytes .../chat_item_pale_slidable_buttons.png | Bin 0 -> 3238 bytes .../golden/chat_item_slidable_actions.png | Bin 0 -> 3424 bytes 10 files changed, 586 insertions(+), 18 deletions(-) create mode 100644 test/src/components/chat_item/chat_item_test.dart create mode 100644 test/src/components/chat_item/golden/chat_item_custom_leading.png create mode 100644 test/src/components/chat_item/golden/chat_item_custom_slidable_buttons.png create mode 100644 test/src/components/chat_item/golden/chat_item_default.png create mode 100644 test/src/components/chat_item/golden/chat_item_highlighted.png create mode 100644 test/src/components/chat_item/golden/chat_item_pale_and_regular_buttons.png create mode 100644 test/src/components/chat_item/golden/chat_item_pale_slidable_buttons.png create mode 100644 test/src/components/chat_item/golden/chat_item_slidable_actions.png diff --git a/example/lib/pages/components/chat_item_example.dart b/example/lib/pages/components/chat_item_example.dart index 8c76d190..81263db7 100644 --- a/example/lib/pages/components/chat_item_example.dart +++ b/example/lib/pages/components/chat_item_example.dart @@ -3,7 +3,7 @@ import 'package:zeta_example/widgets.dart'; import 'package:zeta_flutter/zeta_flutter.dart'; class ChatItemExample extends StatelessWidget { - static const String name = 'Chat Item'; + static const String name = 'ChatItem'; const ChatItemExample({Key? key}) : super(key: key); @@ -14,6 +14,31 @@ class ChatItemExample extends StatelessWidget { child: SingleChildScrollView( child: Column( children: [ + ZetaChatItem( + leading: const ZetaAvatar(initials: 'AZ'), + slidableActions: [ + ZetaSlidableAction( + onPressed: () {}, + paleColor: true, + icon: Icons.star, + ), + ZetaSlidableAction( + onPressed: () {}, + paleColor: true, + icon: Icons.delete, + ), + ZetaSlidableAction( + onPressed: () {}, + icon: Icons.call, + ), + ZetaSlidableAction( + onPressed: () {}, + icon: Icons.message, + ), + ], + title: Text('title'), + subtitle: Text('subtitle'), + ), ZetaChatItem( explicitChildNodes: false, time: DateTime.now(), @@ -22,6 +47,7 @@ class ChatItemExample extends StatelessWidget { leading: const ZetaAvatar(initials: 'AZ'), count: 100, onTap: () {}, + paleButtonColors: true, slidableActions: [ ZetaSlidableAction.menuMore(onPressed: () {}), ZetaSlidableAction.call(onPressed: () {}), diff --git a/lib/src/components/chat_item/chat_item.dart b/lib/src/components/chat_item/chat_item.dart index 1bba9db3..d447b288 100644 --- a/lib/src/components/chat_item/chat_item.dart +++ b/lib/src/components/chat_item/chat_item.dart @@ -27,6 +27,7 @@ class ZetaChatItem extends ZetaStatelessWidget { this.starred, this.slidableActions = const [], this.explicitChildNodes = true, + this.paleButtonColors, @Deprecated('Use slidableActions instead.' ' This variable has been replaced as of 0.12.1') this.onMenuMoreTap, @Deprecated('Use slidableActions instead.' ' This variable has been replaced as of 0.12.1') this.onCallTap, @Deprecated('Use slidableActions instead.' ' This variable has been replaced as of 0.12.1') this.onDeleteTap, @@ -78,6 +79,11 @@ class ZetaChatItem extends ZetaStatelessWidget { /// Whether to show explicit child nodes in the semantics tree. final bool explicitChildNodes; + /// Whether to apply pale color. + /// + /// Pale buttons was the default behavior before 0.15.2, but now buttons have darker colors by default. + final bool? paleButtonColors; + /// Callback for slidable action - menu more. @Deprecated('Use slidableActions instead.' ' This variable has been replaced as of 0.12.1') final VoidCallback? onMenuMoreTap; @@ -109,8 +115,7 @@ class ZetaChatItem extends ZetaStatelessWidget { final actionWith = slidableActionsCount * ZetaSpacing.xl_10; final maxButtonWidth = actionWith / maxScreenWidth; final extend = actionWith / maxScreenWidth; - - return extend.clamp(0, maxButtonWidth); + return extend.clamp(0, maxButtonWidth).toDouble(); } Widget? get _formatLeading { @@ -126,6 +131,7 @@ class ZetaChatItem extends ZetaStatelessWidget { final actions = [...slidableActions]; + // coverage:ignore-start if (onMenuMoreTap != null) { actions.add( ZetaSlidableAction(onPressed: onMenuMoreTap, color: colors.purple, icon: ZetaIcons.more_vertical), @@ -144,6 +150,7 @@ class ZetaChatItem extends ZetaStatelessWidget { if (onDeleteTap != null) { actions.add(ZetaSlidableAction(onPressed: onDeleteTap, color: colors.red, icon: ZetaIcons.delete)); } + // coverage:ignore-end return ZetaRoundedScope( rounded: context.rounded, @@ -154,12 +161,18 @@ class ZetaChatItem extends ZetaStatelessWidget { builder: (context, constraints) { return Slidable( enabled: actions.isNotEmpty, - endActionPane: ActionPane( - extentRatio: - _getSlidableExtend(slidableActionsCount: actions.length, maxScreenWidth: constraints.maxWidth), - motion: const ScrollMotion(), - children: actions, - ), + endActionPane: actions.isEmpty + ? null + : ActionPane( + extentRatio: _getSlidableExtend( + slidableActionsCount: actions.length, + maxScreenWidth: constraints.maxWidth, + ), + motion: const ScrollMotion(), + children: paleButtonColors != null + ? actions.map((action) => action.copyWith(paleColor: paleButtonColors)).toList() + : actions, + ), child: ColoredBox( color: highlighted ? colors.blue.shade10 : colors.surfacePrimary, child: Material( @@ -329,7 +342,8 @@ class ZetaChatItem extends ZetaStatelessWidget { ..add(ObjectFlagProperty.has('onCallTap', onCallTap)) ..add(ObjectFlagProperty.has('onDeleteTap', onDeleteTap)) ..add(ObjectFlagProperty.has('onPttTap', onPttTap)) - ..add(DiagnosticsProperty('explicitChildNodes', explicitChildNodes)); + ..add(DiagnosticsProperty('explicitChildNodes', explicitChildNodes)) + ..add(DiagnosticsProperty('paleButtonColors', paleButtonColors)); } } @@ -360,21 +374,35 @@ class ZetaSlidableAction extends StatelessWidget { super.key, this.onPressed, required this.icon, - this.color, + this.color = ZetaColorBase.blue, this.customForegroundColor, this.customBackgroundColor, this.semanticLabel, + this.paleColor = false, }) : _type = _ZetaSlidableActionType.custom, assert( color != null || (customForegroundColor != null && customBackgroundColor != null), 'Ensure either color, or both customForegroundColor and customBackgroundColor are provided.', ); + const ZetaSlidableAction._({ + super.key, + this.onPressed, + required this.icon, + this.color, + this.customForegroundColor, + this.customBackgroundColor, + this.semanticLabel, + this.paleColor = false, + _ZetaSlidableActionType? type, + }) : _type = type ?? _ZetaSlidableActionType.custom; + /// Constructs a More menu [ZetaSlidableAction]. const ZetaSlidableAction.menuMore({ super.key, this.onPressed, this.semanticLabel = 'More', + this.paleColor = false, }) : icon = ZetaIcons.more_vertical, color = null, customForegroundColor = null, @@ -386,6 +414,7 @@ class ZetaSlidableAction extends StatelessWidget { super.key, this.onPressed, this.semanticLabel = 'Call', + this.paleColor = false, }) : icon = ZetaIcons.phone, color = null, customForegroundColor = null, @@ -397,6 +426,7 @@ class ZetaSlidableAction extends StatelessWidget { super.key, this.onPressed, this.semanticLabel = 'PTT', + this.paleColor = false, }) : icon = ZetaIcons.ptt, color = null, customForegroundColor = null, @@ -408,6 +438,7 @@ class ZetaSlidableAction extends StatelessWidget { super.key, this.onPressed, this.semanticLabel = 'Delete', + this.paleColor = false, }) : icon = ZetaIcons.delete, color = null, customForegroundColor = null, @@ -448,6 +479,9 @@ class ZetaSlidableAction extends StatelessWidget { /// {@macro zeta-widget-semantic-label} final String? semanticLabel; + /// Whether to apply pale color. + final bool paleColor; + @override Widget build(BuildContext context) { return Expanded( @@ -462,8 +496,9 @@ class ZetaSlidableAction extends StatelessWidget { child: IconButton( onPressed: () => onPressed?.call(), style: IconButton.styleFrom( - backgroundColor: customBackgroundColor ?? (color ?? _type.getColor(context)).shade10, - foregroundColor: customForegroundColor ?? (color ?? _type.getColor(context)).shade60, + backgroundColor: customBackgroundColor ?? (color ?? _type.getColor(context)).shade(paleColor ? 10 : 60), + foregroundColor: customForegroundColor ?? + (paleColor ? (color ?? _type.getColor(context)).shade60 : Zeta.of(context).colors.surfaceDefault), shape: const RoundedRectangleBorder(borderRadius: ZetaRadius.minimal), side: BorderSide.none, ), @@ -475,16 +510,39 @@ class ZetaSlidableAction extends StatelessWidget { ); } + /// Creates a copy of this [ZetaSlidableAction] but with the given fields replaced with the new values. + ZetaSlidableAction copyWith({ + VoidCallback? onPressed, + IconData? icon, + Color? customForegroundColor, + Color? customBackgroundColor, + ZetaColorSwatch? color, + String? semanticLabel, + bool? paleColor, + }) { + return ZetaSlidableAction._( + key: key, + onPressed: onPressed ?? this.onPressed, + icon: icon ?? this.icon, + customForegroundColor: customForegroundColor ?? this.customForegroundColor, + customBackgroundColor: customBackgroundColor ?? this.customBackgroundColor, + color: color ?? this.color, + semanticLabel: semanticLabel ?? this.semanticLabel, + paleColor: paleColor ?? this.paleColor, + type: _type, + ); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(ObjectFlagProperty.has('onPressed', onPressed)) - ..add(DiagnosticsProperty('icon', icon)) - ..add(ColorProperty('foregroundColor', customForegroundColor)) - ..add(ColorProperty('backgroundColor', customBackgroundColor)) - ..add(DiagnosticsProperty('icon', icon)) + ..add(DiagnosticsProperty('paleColor', paleColor)) + ..add(StringProperty('semanticLabel', semanticLabel)) ..add(ColorProperty('color', color)) - ..add(StringProperty('semanticLabel', semanticLabel)); + ..add(ColorProperty('customBackgroundColor', customBackgroundColor)) + ..add(ColorProperty('customForegroundColor', customForegroundColor)) + ..add(DiagnosticsProperty('icon', icon)); } } diff --git a/test/src/components/chat_item/chat_item_test.dart b/test/src/components/chat_item/chat_item_test.dart new file mode 100644 index 00000000..d2a685b5 --- /dev/null +++ b/test/src/components/chat_item/chat_item_test.dart @@ -0,0 +1,484 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/tolerant_comparator.dart'; +import '../../../test_utils/utils.dart'; + +void main() { + setUpAll(() { + final testUri = Uri.parse(getCurrentPath('chat_item')); + goldenFileComparator = TolerantComparator(testUri, tolerance: 0.01); + }); + + group('ZetaChatItem Tests', () { + testWidgets('ZetaChatItem displays correctly', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + final timeFormat = DateFormat('hh:mm a'); + + await tester.pumpWidget( + TestApp( + home: Scaffold( + body: Column( + children: [ + ZetaChatItem( + explicitChildNodes: false, + time: time, + enabledWarningIcon: true, + enabledNotificationIcon: true, + leading: const ZetaAvatar(initials: 'AZ'), + count: 100, + onTap: () {}, + paleButtonColors: true, + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ), + ); + + // Verify that the title, subtitle, and time are displayed correctly + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('Hello, how are you?'), findsOneWidget); + expect(find.text(timeFormat.format(time)), findsOneWidget); + + final chatItemFinder = find.byType(ZetaChatItem); + + // Verify that the widget is tappable + await tester.tap(chatItemFinder); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_default.png')), + ); + }); + }); + + testWidgets('ZetaChatItem highlighted', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + final timeFormat = DateFormat('hh:mm a'); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + explicitChildNodes: false, + time: time, + enabledWarningIcon: true, + enabledNotificationIcon: true, + leading: const ZetaAvatar(initials: 'AZ'), + count: 100, + onTap: () {}, + paleButtonColors: false, + starred: true, + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + highlighted: true, + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + // Verify that the title, subtitle, and time are displayed correctly + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('Hello, how are you?'), findsOneWidget); + expect(find.text(timeFormat.format(time)), findsOneWidget); + + final chatItemFinder = find.byType(ZetaChatItem); + + // Verify that the widget is tappable + await tester.tap(chatItemFinder); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_highlighted.png')), + ); + }); + + testWidgets('ZetaChatItem slidable actions', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + time: time, + leading: const ZetaAvatar(initials: 'AZ'), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + final chatItemFinder = find.byType(ZetaChatItem); + + await tester.drag(chatItemFinder, const Offset(-200, 0)); + await tester.pumpAndSettle(); + + // Verify that the slidable actions are displayed correctly + expect(find.byIcon(ZetaIcons.more_vertical_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.phone_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.ptt_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.delete_round), findsOneWidget); + + // Verify that tapping on the slidable actions triggers the corresponding callbacks + await tester.tap(find.byIcon(ZetaIcons.more_vertical_round)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(ZetaIcons.phone_round)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(ZetaIcons.ptt_round)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(ZetaIcons.delete_round)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_slidable_actions.png')), + ); + }); + + testWidgets('ZetaChatItem with pale slidable button colors', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + time: time, + leading: const ZetaAvatar(initials: 'AZ'), + paleButtonColors: true, + slidableActions: [ + ZetaSlidableAction.menuMore( + onPressed: () {}, + ), + ZetaSlidableAction.call( + onPressed: () {}, + ), + ZetaSlidableAction.ptt( + onPressed: () {}, + ), + ZetaSlidableAction.delete( + onPressed: () {}, + ), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + final chatItemFinder = find.byType(ZetaChatItem); + + await tester.drag(chatItemFinder, const Offset(-200, 0)); + await tester.pumpAndSettle(); + + // Verify that the slidable actions have pale button colors + expect(find.byIcon(ZetaIcons.more_vertical_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.phone_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.ptt_round), findsOneWidget); + expect(find.byIcon(ZetaIcons.delete_round), findsOneWidget); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_pale_slidable_buttons.png')), + ); + }); + + testWidgets('ZetaChatItem with 2 pale buttons and 2 regular buttons', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + time: time, + leading: const ZetaAvatar(initials: 'AZ'), + slidableActions: [ + ZetaSlidableAction( + onPressed: () {}, + paleColor: true, + icon: Icons.star, + ), + ZetaSlidableAction( + onPressed: () {}, + paleColor: true, + icon: Icons.delete, + ), + ZetaSlidableAction( + onPressed: () {}, + icon: Icons.call, + ), + ZetaSlidableAction( + onPressed: () {}, + icon: Icons.message, + ), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + final chatItemFinder = find.byType(ZetaChatItem); + + await tester.drag(chatItemFinder, const Offset(-200, 0)); + await tester.pumpAndSettle(); + + // Verify that the slidable actions are displayed correctly + expect(find.byIcon(Icons.star), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + expect(find.byIcon(Icons.call), findsOneWidget); + expect(find.byIcon(Icons.message), findsOneWidget); + + // Verify that the pale buttons have the correct color + final paleButtons = tester.widgetList( + find.byWidgetPredicate((widget) => widget is ZetaSlidableAction && widget.paleColor), + ); + expect(paleButtons.length, 2); + + // Verify that the non-pale buttons have the correct color + final nonPaleButtons = tester.widgetList( + find.byWidgetPredicate((widget) => widget is ZetaSlidableAction && !widget.paleColor), + ); + expect(nonPaleButtons.length, 2); + + // Verify that tapping on the slidable actions triggers the corresponding callbacks + await tester.tap(find.byIcon(Icons.star)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(Icons.delete)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(Icons.call)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(Icons.message)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_pale_and_regular_buttons.png')), + ); + }); + + testWidgets('ZetaChatItem with custom leading widget', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + time: time, + leading: Container( + width: 40, + height: 40, + color: Colors.blue, + ), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + final chatItemFinder = find.byType(ZetaChatItem); + + // Verify that the custom leading widget is displayed correctly + expect(find.byType(Container), findsOneWidget); + expect(find.byWidgetPredicate((widget) => widget is Container && widget.color == Colors.blue), findsOneWidget); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_custom_leading.png')), + ); + }); + + testWidgets('ZetaChatItem with custom slidable buttons', (WidgetTester tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + const title = Text('John Doe'); + const subtitle = Text('Hello, how are you?'); + final time = DateTime.now(); + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaChatItem( + time: time, + leading: const ZetaAvatar(initials: 'AZ'), + slidableActions: [ + ZetaSlidableAction( + onPressed: () {}, + color: ZetaColorBase.orange, + icon: Icons.star, + ), + ZetaSlidableAction( + onPressed: () {}, + color: ZetaColorBase.red, + icon: Icons.delete, + ), + ], + title: title, + subtitle: subtitle, + ), + ], + ), + ), + ); + + final chatItemFinder = find.byType(ZetaChatItem); + + await tester.drag(chatItemFinder, const Offset(-200, 0)); + await tester.pumpAndSettle(); + + // Verify that the slidable actions are displayed correctly + expect(find.byIcon(Icons.star), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + + // Verify that tapping on the slidable actions triggers the corresponding callbacks + await tester.tap(find.byIcon(Icons.star)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await tester.tap(find.byIcon(Icons.delete)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await expectLater( + chatItemFinder, + matchesGoldenFile(join(getCurrentPath('chat_item'), 'chat_item_custom_slidable_buttons.png')), + ); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnosticsZetaChatItem = DiagnosticPropertiesBuilder(); + final time = DateTime.now(); + ZetaChatItem( + title: const Text('Title'), + subtitle: const Text('Subtitle'), + time: time, + leading: const ZetaAvatar(initials: 'AZ'), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + count: 1, + enabledNotificationIcon: true, + highlighted: true, + enabledWarningIcon: true, + starred: true, + ).debugFillProperties(diagnosticsZetaChatItem); + + expect(diagnosticsZetaChatItem.finder('rounded'), 'null'); + expect(diagnosticsZetaChatItem.finder('highlighted'), 'true'); + expect(diagnosticsZetaChatItem.finder('time'), time.toString()); + expect(diagnosticsZetaChatItem.finder('timeFormat'), 'null'); + expect(diagnosticsZetaChatItem.finder('enabledWarningIcon'), 'true'); + expect(diagnosticsZetaChatItem.finder('enabledNotificationIcon'), 'true'); + expect(diagnosticsZetaChatItem.finder('count'), '1'); + expect(diagnosticsZetaChatItem.finder('onTap'), 'null'); + expect(diagnosticsZetaChatItem.finder('starred'), 'true'); + expect(diagnosticsZetaChatItem.finder('onMenuMoreTap'), 'null'); + expect(diagnosticsZetaChatItem.finder('onCallTap'), 'null'); + expect(diagnosticsZetaChatItem.finder('onDeleteTap'), 'null'); + expect(diagnosticsZetaChatItem.finder('onPttTap'), 'null'); + expect(diagnosticsZetaChatItem.finder('explicitChildNodes'), 'true'); + expect(diagnosticsZetaChatItem.finder('paleButtonColors'), 'null'); + + final diagnosticsZetaSlidableAction = DiagnosticPropertiesBuilder(); + const ZetaSlidableAction(icon: Icons.star).debugFillProperties(diagnosticsZetaSlidableAction); + + expect(diagnosticsZetaSlidableAction.finder('onPressed'), 'null'); + expect(diagnosticsZetaSlidableAction.finder('icon'), 'IconData(U+0E5F9)'); + expect(diagnosticsZetaSlidableAction.finder('foregroundColor'), null); + expect(diagnosticsZetaSlidableAction.finder('backgroundColor'), null); + expect(diagnosticsZetaSlidableAction.finder('color'), ZetaColorBase.blue.toString()); + expect(diagnosticsZetaSlidableAction.finder('semanticLabel'), 'null'); + expect(diagnosticsZetaSlidableAction.finder('paleColor'), 'false'); + }); +} diff --git a/test/src/components/chat_item/golden/chat_item_custom_leading.png b/test/src/components/chat_item/golden/chat_item_custom_leading.png new file mode 100644 index 0000000000000000000000000000000000000000..668000b0575d0acb51c103d61a14c807aa6154f0 GIT binary patch literal 2486 zcmeAS@N?(olHy`uVBq!ia0y~yV0;L~4>;I>B9p|ft_D(!#X;^)4C~IxyaaMsik&<| zIDnvrBc+3ZfiuC=#WAE}&fB|t`=*34G(6n=Ca=s_LGZ>j9by`WaZFOlN>e+( zCobfj$~rw^V+CiH&Vdw0?#*uALR>vZchx5+K8-ipto_@__|fcH^G-`1tv6zh1v~?BKzJS04YLv)PW1Z`uB1 zN4v$3KYRA<%H#6#^5dJ+&!T6eYO4JA=laKEv&~MVqPk+s--J2V<$13@ z-s?kzh|(bj0YN+gcICgdR6^jL`@cSkto!s+Gv@KporQmI{doFZ{&nh~R&}V(X;z0e z*F1l`$m*n|&Y_>WhvXW5ZoM*}bH4rmSKi-$9?($1TNYv~6F^s6fDL$kJ*5nA+!B zr^lc1Vv?4Yetfq)@0`v14<8D843E8fwW_jy^Tv&ZYb-w($hZCd`+QF2jr*DNxqXsT zf$8p*E5iY!Ph}=plN-p2$lLetufNW~;C*s3_B4s?4lGG_R2pZ{;z_rop*9+7Nag-$ iau`hx!#z1X;WwBgIQP3PTN1GC#o+1c=d#Wzp$P!Ve!yA) literal 0 HcmV?d00001 diff --git a/test/src/components/chat_item/golden/chat_item_custom_slidable_buttons.png b/test/src/components/chat_item/golden/chat_item_custom_slidable_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..47655b7bcaa07343d78d62c9b33bb73ca41fa0ff GIT binary patch literal 3034 zcmeHJ`BT$J6#qtApdcV3n39k<9>f*|f|lSYd~M_q%i&PG2zM$+L8wGP2@q;UiRKt- zLBb)ejCMv5IRqq-1ObE5kbsd(i~%DC3;`oTAQz2eJM9np7nC=%^Jd@Mo!R*uyGuS0 z>~CRaZw3H>MZi9vPym388^)Nav7u6AH*nLCAcRnVEKneMELLm;KL>_bK7;lkmb9;QI?~Kbj?AFimajVHrdp7u9V8+T;QTGStuL)wt z_1-}H6_l3N#f;Os_d$Q77u5D8uIdim@BoX)ezn%jcI$Qp#XNEy^aa0ihR1}neC zH;qE4UY*@XCc}H%--Nn9+FW*ee2cZQ=KwiNQsZ9=HIKa6 zilJ{YY%nCU8xZCL1vYM64Om+n=NFn9?eRGa*|4z_SYsUv6c)MxKJVKwIRp^wz+HEN zUY6ttFU%-kRI6wz8msv#G0Y?4C;j3(FEFCtOP?_t!sQJ`MfZDgp?K+ct~$l0WkPCd zDqkq1TfjV=<@doEHJW?(Ws#6nGhY`@gYRq@YfJ(ka)Zjb{$ZwRG}R2kLp_4R12{_*e$mBLUR4bjGGiE6T~7*9caM3K6JoY=e`C0|5B$SPZ^ z5k~skX(X9OI(AQHm#byxM#DrRPGqMNL!X-RU!SYBJv@yNf}A_3Rf-Y9eb1AVOTNhE zv0tnx@>*7We-Aj{15!s*aoWcC%LGfILr!*JP|&s{i?C-lAAs#f#zSf#Av$fzF-ezr zz93iXl;xHFG@)$sa{C1C5&Jw*z=riCAeRP`s*=#{fA9Jj9q-wS9)$eKs}^yN7DVPX zn-Oa&-k(A%oki;Upq`>rs1JlbD5Jj#C%J3|oQM2W3Bd35uOwd>b)bqlG~**iE=#W? zrJY_Fmt@7xw#=%9(~iBgHa14UR+b&RC2rR=h`fbyWVa6cy19oeysVfgM>`IV5O2d0 zW?8ieJTC`s0bKHB*?8w^SxRY@K91+=jlp0H)a%sOn(|uZT&UDPYdI!ml$jBbTp{T^ zq|qMIFo7p_w&M&2L&bz5~y%7{CAX}-Kf`vMhm1&*iG z$9A1Ncod_@$kPWsIaWBu98Mu$bpVF}K``11a)6=h5Tzqp@dS@Yo0$`OhDxWI=&j<# zGkm;hpEAqXI#^33j_!uGV;+(VEB6}~8`4^XF@qFOBR|y3v!YA*?qrWycHmh^aK;o> zBe!g*gxK1X@B%eT!Cc;mjAqvD{@$o!~akYZ;=bZ$BDfUvQ8TQ=RkmO Kuum;E=FDH|sP`}c literal 0 HcmV?d00001 diff --git a/test/src/components/chat_item/golden/chat_item_default.png b/test/src/components/chat_item/golden/chat_item_default.png new file mode 100644 index 0000000000000000000000000000000000000000..f6fa9d75869f8096c8d35ca4a351630db214b84e GIT binary patch literal 3910 zcmeH~dr;Ep8pnTBlb%dH=5D4L?%B@CIqR0@B{vnFH8+oWD-*9RR$3G!bk}DAfVz8CWlP`09c0nBPa?0Ad_o)Z@zIYb7$*F&DsV@iwX_|Y6l$W)`AUbfg#c6 zYb)D4yFA}i zwCg0_Y}cW;L2mn2C>yqvZG+OTmhZ~A_&#?jD8#($)(yY94TqEeI&^4Tx|3G^i{op3fp8Kb8#B!gk$V2dIDch5^72%%E*R z;OC^P-l5!F?BE^lb(g^i;VfWgBB~gSqZ1Kuwvg3$anJ&A{GM*a#|00Ep^#6vi{mw8 zEniI&n_4FZ9_E+Q=YE5;4b&?sQZ(=uwv?j#Vy z3*6b>=u!KoIiCOVwEHX$O&GdKh8(Ar>{mt0H|HI7H8Em2^@xn+*;6LM%3x%40|y4G zH%e6FQmC*5ujQqjut8h}lHMxQ(aGW0APk0rD8Y`ls7x?$Nn;qF0M$($+IkaFr!@b2T zI+`E}RK=!2O(%X)h+10DPj>VBtT*O5JS(JudZR=BK~@i6X^rwk)ls_25AN=H+R_sL zg5p$$*Y>7FAqPw8cx}<-w%RsD>-gB%nMTT~#W{8o)7qfb;?*Ba&!*E|l_O|!ILTL< z#$#0ME^78-jGu|`Vv~^3NifdQKUAh``y|C-But7ZlX;p0_~I@XpWdRkS~>JAZ%Uf( zn>G=dtq^H-*Kx;;!@?icL*Awi#NrJ!GC}iA2{)5$a%)jeJIkhfBf~g`Hi@wo|NbJk z5U(x95|d%N*<@DFuYJ3{1v@Ez<0r!c;MRT%zgJU9COxUq-|)aXf@8R1VJ8hH&8w@a zYV^+3l{doSu%-#c z@EkGWV;$r$nD4=(ZeI9qe|kn7N!QP!jnZAZg)VrMsw4}H(b*VOALPs0pA2z-| zy}xvnT9FsJa-4itzBv}V(b|oT=@TCCiETS1!e+u3-Gyyam5r@)_boEdCyPGfs}!rZ zcd`WN09)~tX1((9MCe-ue4Q0FQmlD%1)~J3I@)i^nV=6?T^4}AWg7xe*Gseg?*2ig z*o|j)WAvwl$&KvgF^JW}CP{M)ceQ}}IBzdCk?q8Xc|Z^7&#{-NH8p-2aVMQths+XT z!I=Ul`I|EZ!*$6oq`f3<)v{Z`TPIO^_!>Cn3a!CJy2!g8`MqZNDSm<>=^!)^mCoKZ z-cvt~j@lq*eq$}I-Bh21AHLnXR>prufOPD#&_S^_3 zfDn$~rhmo0S6iMoQf3a#R+Z%Do?o@<-V5~gg{^F=<)~*mc(h!V+4)l}WOt39YurrT z`tu7L%_NS>k-?3241^H!zm$0b^DO<$Pa>maLn)tJ(dlTg96vJrze-t}FEeLb0yE z-DczQcdrf4naRb$vgg-rZ|>-a?#HKen4MeSi73n@j>)4zBmp<g%m=jE4W8B1{_zIHK9Mh5#gC5%N0i<9L#d4?kYkz)ngV{BV@HU z!w}1r?AcjeC$1A<-n$aqTuO>ieY}lHmH&qJ6brrgfcOcY`rrl$2!gFLq~aV$e4$+r zc_cu4*XDL0V5A6SJo4*RtC^KOZ3EvqN{D54PZ-iQv+YGF%Fi;MU9CVB{jR7K$oaZ* ztlFaU)ws$$>8Oi@ufg+8G8K8qz`VQ@4j+jT%}6V`_KfIf-2g>!y+*X~ND zW_LJUirA#=yl0n_`?swpfFI_=Tp`~CUH-ft1vpv_Cj&VLdc*#Q$A8ZEzvMpu9{SRk oFMavKz!wI-F!28|uxj=yL9!J067qEIFARkI6cJP#c>L180ZcN`W&i*H literal 0 HcmV?d00001 diff --git a/test/src/components/chat_item/golden/chat_item_highlighted.png b/test/src/components/chat_item/golden/chat_item_highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..27b3a7bd45444cdbf5919b7e1754020f0a50cf59 GIT binary patch literal 4022 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 D0odZV literal 0 HcmV?d00001 diff --git a/test/src/components/chat_item/golden/chat_item_slidable_actions.png b/test/src/components/chat_item/golden/chat_item_slidable_actions.png new file mode 100644 index 0000000000000000000000000000000000000000..797a78f24e97b0068a080388e735bd8582c63cae GIT binary patch literal 3424 zcmeHKeNfVA9{+i6hP7s$SGl%o=ic7Sl&P^;>MG?bq{mHWqZDqlBB>MNb0(kf{1`1BEtQgcINJ8ZszW<`^)dlXFkvK zndkd`X1??JJ~K~QDhlbdAz%Xl06s~F5{>|X$0Udeh!?2b*rwov54+ zPCKU8U7e?-ppwOC^djSZ@$`)Q_V_(%JM*>EJrqU4fU)jic6N5K*zoe2MmL~|)>X!m zVCQ!3RX$L3KgdrTKT5E7m*F+z_CC5@0apx#qbWZ?@2?j36ngAymS-Lg z+V5ZY%~p{9*aLhK(gSS%Z+Ko81pJzjp3Xc$DNxz9sftM^Rg8GTo2pR^ZO?=?5y#9w zcUya7Hd1;10iO=16_DyuD5z7QmBZSC#rW5>C=mv{3_rdO(CEhiIcboxG~v%mjP z_tF{5_%Y~G04m1NmQQ4llY1zR3uz52vN54aN9JcTiVIYS{(e#2r7m=JL=mRs(zuMr zB4;6LRqg$&+O$-^s@7DE&TmA%I$!zaR3w?_S9GxirP}YD_M_$6$phxW<4X!#;G6H4tZ>e>2kM`>>@Sb^?HG0f9?x;!aj~<}442CWU%Y7ejRx zJ&NBWUi<@8ay91eE2woM4Svg7haWgim%yfoot`_obdU6?+pChkh7b!%$zFTDQ~17s z>Y=d5&|m7V*8WexrG_a@!}BZ16JKdqJpt4lePV{rc&X(5STCU7+w(vb#PdMuzo7p! zhd$T-yFHy8oghp$E7_$QvVG9}y3yjLyMewF%U!lLAB)vn+HSGqRG1op{+67%j570` zm)Sy5eR9X(w4{7lDRX={WQMPIx4!ym(G>w*$*Io(!4Be{wu+2Amlawbo)){3!Ed@m zWK`r)qXU5WX!6nw$#^L|DhijDW-yG9Extu#seLs1GaUuE`von|GD~eWAIRorhon5M z_0RGreq9lGC2THNaD8uNOStk-UBHfv&BQN>J@4`}nF3v3@(89)h;*4#+O>?+jEbAq z{$wQ2aLt{@8XR@AaLSL{_BEoU{ed|2%^(||)*!&? z2kG;0%d;mm%$u3l#T3#`-Ao_N6**A1T;*E>`C$4_lJ?d20i=*YCv~!FU;z@!m1p{7 zQWD>s^}4sedRTxcQN$EyuPPwXX2IIH>*lSWz42GseN4@PBKi>#xK7#U9!CXsYB zN4c{bvZOq2Wb>RrWkV|;y@!GA4SI+ALT4!8A8rccX++15Xd-vJetf-Ml!eiOujU&Ux?@Pu!^k&sSc#}W7i(so7%^S&B^MBZ8U{0q>;_1Kwkc@1klSo6S|2i82W g=7Im82Y!|wn`E(jD)+q&{*r*CL{x$>{*%xC0e-vq=l}o! literal 0 HcmV?d00001