diff --git a/example/lib/pages/components/notification_list_example.dart b/example/lib/pages/components/notification_list_example.dart index a13ae1ad..1b203145 100644 --- a/example/lib/pages/components/notification_list_example.dart +++ b/example/lib/pages/components/notification_list_example.dart @@ -86,6 +86,7 @@ class NotificationListItemExample extends StatelessWidget { leading: ZetaNotificationBadge.avatar( avatar: ZetaAvatar.initials( initials: "JS", + semanticUpperBadgeLabel: 'Urgent', lowerBadge: ZetaAvatarBadge.icon( color: ZetaColors().surfacePositive, icon: Icons.check, diff --git a/lib/src/components/avatars/avatar.dart b/lib/src/components/avatars/avatar.dart index 7ca1443f..726c381b 100644 --- a/lib/src/components/avatars/avatar.dart +++ b/lib/src/components/avatars/avatar.dart @@ -53,9 +53,9 @@ class ZetaAvatar extends ZetaStatelessWidget { this.lowerBadge, this.upperBadge, this.borderColor, - this.semanticLabel = 'avatar', - this.semanticUpperBadgeLabel = 'upperBadge', - this.semanticLowerBadgeLabel = 'lowerBadge', + this.semanticLabel, + this.semanticUpperBadgeLabel, + this.semanticLowerBadgeLabel, this.initialTextStyle, this.label, this.labelTextStyle, @@ -149,17 +149,17 @@ class ZetaAvatar extends ZetaStatelessWidget { /// {@template zeta-widget-semantic-label} /// This label is used by accessibility frameworks (e.g. TalkBack on Android) to describe the component. /// {@endtemplate} - final String semanticLabel; + final String? semanticLabel; /// Value passed into wrapping [Semantics] widget for lower badge. /// /// {@macro zeta-widget-semantic-label} - final String semanticLowerBadgeLabel; + final String? semanticLowerBadgeLabel; /// Value passed into wrapping [Semantics] widget for upper badge. /// /// {@macro zeta-widget-semantic-label} - final String semanticUpperBadgeLabel; + final String? semanticUpperBadgeLabel; /// Text style for initials. /// diff --git a/lib/src/components/badges/indicator.dart b/lib/src/components/badges/indicator.dart index abb04172..efbb3d4e 100644 --- a/lib/src/components/badges/indicator.dart +++ b/lib/src/components/badges/indicator.dart @@ -92,6 +92,7 @@ class ZetaIndicator extends ZetaStatelessWidget { int? value, bool? inverse, Key? key, + String? semanticLabel, }) { return ZetaIndicator( key: key ?? this.key, @@ -100,6 +101,7 @@ class ZetaIndicator extends ZetaStatelessWidget { icon: icon ?? this.icon, value: value ?? this.value, inverse: inverse ?? this.inverse, + semanticLabel: semanticLabel ?? this.semanticLabel, ); } @@ -111,18 +113,21 @@ class ZetaIndicator extends ZetaStatelessWidget { final sizePixels = _getSizePixels(size, type, context); return Semantics( - value: semanticLabel ?? value?.toString() ?? '', + label: semanticLabel, + container: true, child: Container( width: sizePixels + Zeta.of(context).spacing.minimum, height: sizePixels + Zeta.of(context).spacing.minimum, - decoration: BoxDecoration( - border: Border.all( - width: ZetaBorders.medium, - color: Zeta.of(context).colors.borderSubtle, - ), - color: (inverse ? foregroundColor : Colors.transparent), - borderRadius: Zeta.of(context).radius.full, - ), + decoration: type == ZetaIndicatorType.icon + ? BoxDecoration( + border: Border.all( + width: ZetaBorders.medium, + color: Zeta.of(context).colors.borderSubtle, + ), + color: (inverse ? foregroundColor : Colors.transparent), + borderRadius: Zeta.of(context).radius.full, + ) + : null, child: Center( child: Container( width: sizePixels, @@ -155,11 +160,15 @@ class ZetaIndicator extends ZetaStatelessWidget { ); case ZetaIndicatorType.notification: return Center( - child: Text( - value.formatMaxChars(), - style: ZetaTextStyles.labelIndicator.copyWith( - color: foregroundColor, - height: size == ZetaWidgetSize.large ? 1 : (12 / 16), + child: ExcludeSemantics( + excluding: semanticLabel != null, + child: Text( + value.formatMaxChars(), + style: ZetaTextStyles.labelIndicator.copyWith( + color: foregroundColor, + fontSize: size == ZetaWidgetSize.large ? 12 : 11, + height: size == ZetaWidgetSize.large ? 1 : (0.5 / 16), + ), ), ), ); diff --git a/lib/src/components/components.dart b/lib/src/components/components.dart index 63bae3a4..af26f8cd 100644 --- a/lib/src/components/components.dart +++ b/lib/src/components/components.dart @@ -32,7 +32,7 @@ export 'in_page_banner/in_page_banner.dart'; export 'list_item/dropdown_list_item.dart'; export 'list_item/list_item.dart'; export 'list_item/notification_list_item.dart'; -export 'navigation bar/navigation_bar.dart'; +export 'navigation bar/navigation_bar.dart' hide NavigationItem; export 'navigation_rail/navigation_rail.dart'; export 'pagination/pagination.dart'; export 'password/password_input.dart'; diff --git a/lib/src/components/navigation bar/navigation_bar.dart b/lib/src/components/navigation bar/navigation_bar.dart index 5d530614..e8f16866 100644 --- a/lib/src/components/navigation bar/navigation_bar.dart +++ b/lib/src/components/navigation bar/navigation_bar.dart @@ -131,7 +131,7 @@ class ZetaNavigationBar extends ZetaStatelessWidget { final index = items.indexOf(navItem); return Expanded( flex: !shrinkItems ? 1 : 0, - child: _NavigationItem( + child: NavigationItem( selected: index == currentIndex, item: navItem, onTap: () => onTap?.call(index), @@ -186,7 +186,7 @@ class ZetaNavigationBar extends ZetaStatelessWidget { } return Container( - padding: EdgeInsets.symmetric(horizontal: Zeta.of(context).spacing.medium), + padding: EdgeInsets.symmetric(horizontal: Zeta.of(context).spacing.large), decoration: BoxDecoration( color: colors.surfacePrimary, border: Border(top: BorderSide(color: colors.borderSubtle)), @@ -210,19 +210,32 @@ class ZetaNavigationBar extends ZetaStatelessWidget { } } -class _NavigationItem extends ZetaStatelessWidget { - const _NavigationItem({ +/// A single item in a [ZetaNavigationBar]. +@visibleForTesting +@protected +class NavigationItem extends ZetaStatelessWidget { + /// Creates a new [NavigationItem]. + const NavigationItem({ + super.key, required this.selected, required this.item, required this.onTap, required this.context, }); + /// Whether the item is selected. final bool selected; + + /// The item to display. final ZetaNavigationBarItem item; + + /// Called when the item is tapped. final VoidCallback onTap; + + /// The build context of the [ZetaNavigationBar]. final BuildContext context; + /// The badge to show on the navigation item. Widget get badge { final ZetaColors colors = Zeta.of(context).colors; return Positioned( @@ -230,7 +243,7 @@ class _NavigationItem extends ZetaStatelessWidget { right: Zeta.of(context).spacing.minimum, child: DecoratedBox( decoration: BoxDecoration( - color: colors.surfacePrimary, + color: colors.surfaceDefault, borderRadius: Zeta.of(context).radius.full, ), child: item.badge?.copyWith( @@ -240,6 +253,7 @@ class _NavigationItem extends ZetaStatelessWidget { ? ZetaWidgetSize.medium : null, type: ZetaIndicatorType.notification, + semanticLabel: item.badge?.semanticLabel, ), ), ); @@ -255,9 +269,10 @@ class _NavigationItem extends ZetaStatelessWidget { child: InkResponse( borderRadius: context.rounded ? Zeta.of(context).radius.rounded : Zeta.of(context).radius.none, onTap: onTap, + hoverColor: colors.surfaceHover, + highlightShape: BoxShape.rectangle, child: Semantics( button: true, - explicitChildNodes: true, label: item.label, child: Container( padding: EdgeInsets.only( diff --git a/test/scripts/output/test_table.md b/test/scripts/output/test_table.md index 307b28b7..4c298e67 100644 --- a/test/scripts/output/test_table.md +++ b/test/scripts/output/test_table.md @@ -1,6 +1,7 @@ | Component | Accessibility | Content | Dimensions | Styling | Interaction | Golden | Performance | Unorganised | Total Tests | | -------------- | ------------- | ------- | ---------- | ------- | ----------- | ------ | ----------- | ----------- | ----------- | | Accordion | 0 | 2 | 0 | 1 | 2 | 0 | 0 | 0 | 5 | +| Avatar Rail | 1 | 2 | 2 | 1 | 2 | 1 | 1 | 0 | 10 | | Avatar | 1 | 3 | 6 | 5 | 0 | 6 | 0 | 0 | 21 | | Indicator | 0 | 7 | 0 | 0 | 0 | 5 | 0 | 0 | 12 | | Label | 0 | 8 | 0 | 0 | 0 | 7 | 0 | 0 | 15 | @@ -11,17 +12,19 @@ | Button | 0 | 10 | 0 | 2 | 1 | 8 | 0 | 0 | 21 | | Chat Item | 0 | 10 | 0 | 0 | 0 | 8 | 0 | 0 | 18 | | Checkbox | 0 | 3 | 0 | 0 | 3 | 3 | 0 | 0 | 9 | -| Chip | 0 | 1 | 0 | 0 | 5 | 0 | 0 | 0 | 6 | +| Chip | 0 | 2 | 0 | 0 | 5 | 0 | 0 | 0 | 7 | +| Status Chip | 1 | 3 | 1 | 3 | 1 | 3 | 0 | 0 | 12 | | Comms Button | 2 | 7 | 0 | 0 | 0 | 1 | 0 | 0 | 10 | | Dialpad | 0 | 2 | 0 | 1 | 2 | 3 | 0 | 0 | 8 | | Fab | 0 | 6 | 0 | 1 | 1 | 6 | 0 | 0 | 14 | | Icon | 1 | 4 | 1 | 6 | 0 | 0 | 0 | 0 | 12 | | In Page Banner | 0 | 4 | 0 | 4 | 2 | 4 | 0 | 0 | 14 | +| Navigation Bar | 5 | 7 | 3 | 4 | 2 | 6 | 0 | 0 | 27 | | Password Input | 0 | 4 | 0 | 0 | 0 | 2 | 0 | 0 | 6 | | Search Bar | 0 | 5 | 0 | 0 | 5 | 5 | 0 | 0 | 15 | | Slider | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | -| Stepper | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 30 | 30 | +| Stepper | 4 | 8 | 4 | 6 | 2 | 6 | 0 | 0 | 30 | | Stepper Input | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 2 | | Tooltip | 0 | 3 | 1 | 3 | 0 | 4 | 0 | 0 | 11 | -| Top App Bar | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 25 | 25 | -| Total Tests | 7 | 95 | 11 | 26 | 23 | 71 | 0 | 55 | 288 | +| Top App Bar | 2 | 6 | 1 | 1 | 6 | 9 | 0 | 0 | 25 | +| Total Tests | 20 | 122 | 22 | 41 | 36 | 96 | 1 | 0 | 338 | diff --git a/test/src/components/avatar/avatar_test.dart b/test/src/components/avatar/avatar_test.dart index 528d5aca..25459f90 100644 --- a/test/src/components/avatar/avatar_test.dart +++ b/test/src/components/avatar/avatar_test.dart @@ -17,7 +17,7 @@ void main() { }); group('Accessibility Tests', () { - testWidgets('ZetaAvatar meets accessibility requirements', (WidgetTester tester) async { + testWidgets('ZetaAvatar meets accessibility requirements', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( const TestApp( @@ -52,9 +52,9 @@ void main() { 'lowerBadge': 'null', 'backgroundColor': 'null', 'statusColor': 'null', - 'semanticUpperBadgeValue': '"upperBadge"', - 'semanticValue': '"avatar"', - 'semanticLowerBadgeValue': '"lowerBadge"', + 'semanticUpperBadgeValue': 'null', + 'semanticValue': 'null', + 'semanticLowerBadgeValue': 'null', 'initialTextStyle': 'null', }; debugFillPropertiesTest( diff --git a/test/src/components/badge/golden/indicator_notification_with_value.png b/test/src/components/badge/golden/indicator_notification_with_value.png new file mode 100644 index 00000000..edd743bf Binary files /dev/null and b/test/src/components/badge/golden/indicator_notification_with_value.png differ diff --git a/test/src/components/badge/indicator_test.dart b/test/src/components/badge/indicator_test.dart index 43c683a8..ba73efff 100644 --- a/test/src/components/badge/indicator_test.dart +++ b/test/src/components/badge/indicator_test.dart @@ -13,7 +13,46 @@ void main() { goldenFileComparator = TolerantComparator(goldenFile.uri); }); - group('Accessibility Tests', () {}); + group('Accessibility Tests', () { + for (int i = 0; i < 10; i++) { + testWidgets('medium notification value $i meets accessibility standards', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaIndicator( + size: ZetaWidgetSize.medium, + value: i, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + + testWidgets('default notification value $i meets accessibility standards', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaIndicator( + value: i, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + } + }); group('Content Tests', () { final debugFillProperties = { 'color': 'MaterialColor(primary value: Color(0xffff9800))', @@ -170,6 +209,11 @@ void main() { 'indicator_icon_values', ); goldenTest(goldenFile, const ZetaIndicator.notification(), 'indicator_notification_default'); + goldenTest( + goldenFile, + const ZetaIndicator.notification(value: 3, size: ZetaWidgetSize.medium), + 'indicator_notification_with_value', + ); goldenTest( goldenFile, const ZetaIndicator.notification( diff --git a/test/src/components/navigation_bar/golden/navigation_bar_action.png b/test/src/components/navigation_bar/golden/navigation_bar_action.png new file mode 100644 index 00000000..4639ee1c Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_action.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_current_index_0.png b/test/src/components/navigation_bar/golden/navigation_bar_current_index_0.png new file mode 100644 index 00000000..f74cf40a Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_current_index_0.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_current_index_1.png b/test/src/components/navigation_bar/golden/navigation_bar_current_index_1.png new file mode 100644 index 00000000..7eb76580 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_current_index_1.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_current_index_2.png b/test/src/components/navigation_bar/golden/navigation_bar_current_index_2.png new file mode 100644 index 00000000..382e1066 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_current_index_2.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_current_index_3.png b/test/src/components/navigation_bar/golden/navigation_bar_current_index_3.png new file mode 100644 index 00000000..019c7f84 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_current_index_3.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_default.png b/test/src/components/navigation_bar/golden/navigation_bar_default.png new file mode 100644 index 00000000..3f278f56 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_default.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_divider.png b/test/src/components/navigation_bar/golden/navigation_bar_divider.png new file mode 100644 index 00000000..006174fc Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_divider.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_divider_at_0.png b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_0.png new file mode 100644 index 00000000..e7ab82e0 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_0.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_divider_at_1.png b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_1.png new file mode 100644 index 00000000..b123c186 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_1.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_divider_at_2.png b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_2.png new file mode 100644 index 00000000..006174fc Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_2.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_divider_at_3.png b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_3.png new file mode 100644 index 00000000..af3022af Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_divider_at_3.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_shrink_items.png b/test/src/components/navigation_bar/golden/navigation_bar_shrink_items.png new file mode 100644 index 00000000..3f278f56 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_shrink_items.png differ diff --git a/test/src/components/navigation_bar/golden/navigation_bar_split.png b/test/src/components/navigation_bar/golden/navigation_bar_split.png new file mode 100644 index 00000000..04c09638 Binary files /dev/null and b/test/src/components/navigation_bar/golden/navigation_bar_split.png differ diff --git a/test/src/components/navigation_bar/navigation_bar_test.dart b/test/src/components/navigation_bar/navigation_bar_test.dart new file mode 100644 index 00000000..b4d48bfe --- /dev/null +++ b/test/src/components/navigation_bar/navigation_bar_test.dart @@ -0,0 +1,521 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/src/components/navigation%20bar/navigation_bar.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() { + const String parentFolder = 'navigation_bar'; + + const goldenFile = GoldenFiles(component: parentFolder); + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + const items = [ + ZetaNavigationBarItem( + icon: ZetaIcons.star, + label: 'Label0', + badge: ZetaIndicator( + value: 2, + ), + ), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label1', badge: ZetaIndicator(value: 2)), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label2'), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label3'), + ]; + final action = ZetaButton.primary( + label: 'Button', + onPressed: () {}, + ); + + group('Accessibility Tests', () { + testWidgets('meets accessibility requirements', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + currentIndex: 0, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }); + + testWidgets('meets accessibility requirements with action', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar.action( + items: items, + action: action, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }); + + testWidgets('meets accessibility requirements with divider', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const TestApp( + home: ZetaNavigationBar.divided( + items: items, + dividerIndex: 2, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }); + + testWidgets('meets accessibility requirements with split', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const TestApp( + home: ZetaNavigationBar.split( + items: items, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }); + + testWidgets('meets accessibility requirements with shrink items', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + shrinkItems: true, + ), + ), + ); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }); + }); + + group('Content Tests', () { + debugFillPropertiesTest( + ZetaNavigationBar( + items: items, + ), + { + 'items': + "Instance of 'ZetaNavigationBarItem', Instance of 'ZetaNavigationBarItem', Instance of 'ZetaNavigationBarItem', Instance of 'ZetaNavigationBarItem'", + 'currentIndex': 'null', + 'onTap': 'null', + 'splitItems': 'false', + 'dividerIndex': 'null', + 'semanticLabel': 'null', + }, + ); + + testWidgets('renders the correct number of items', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + + final itemsFinder = find.byType(NavigationItem); + expect(itemsFinder, findsNWidgets(items.length)); + }); + + testWidgets('renders the correct label for each item', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + + for (int i = 0; i < items.length; i++) { + final labelFinder = find.text('Label$i'); + expect(labelFinder, findsOneWidget); + } + }); + + testWidgets('renders the correct icon for each item', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + + final iconFinder = find.byElementType(ZetaIcon); + final icons = tester.widgetList(iconFinder).map((e) => e as ZetaIcon).toList(); + for (final icon in icons) { + expect(icon.icon, ZetaIcons.star); + } + }); + testWidgets('passes the correct badge content for each item', (WidgetTester tester) async { + const items1 = [ + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label0', badge: ZetaIndicator(value: 0)), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label1', badge: ZetaIndicator(value: 1)), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label2', badge: ZetaIndicator(value: 2)), + ZetaNavigationBarItem(icon: ZetaIcons.star, label: 'Label3', badge: ZetaIndicator(value: 3)), + ]; + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items1, + ), + ), + ); + + final badgeFinder = find.byType(ZetaIndicator); + final badges = tester.widgetList(badgeFinder).map((e) => e as ZetaIndicator).toList(); + for (int i = 0; i < badges.length; i++) { + expect(badges[i].value, i); + } + }); + + testWidgets('renders the action', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar.action( + items: items, + action: action, + ), + ), + ); + + final buttonFinder = find.byType(ZetaButton); + expect(buttonFinder, findsOneWidget); + }); + + testWidgets('renders the divider', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaNavigationBar.divided( + items: items, + dividerIndex: 2, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final dividerFinder = find.byWidgetPredicate( + (widget) => widget is Container && widget.color == Zeta.of(context).colors.borderSubtle, + ); + + expect(dividerFinder, findsOneWidget); + }); + }); + + group('Dimensions Tests', () { + testWidgets('renders the correct padding', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final containerFinder = find.byType(Container).first; + + expect( + tester.widget(containerFinder).padding, + EdgeInsets.symmetric(horizontal: Zeta.of(context).spacing.large), + ); + }); + + testWidgets('items render the correct padding', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final itemFinder = find.byType(NavigationItem); + if (itemFinder.evaluate().isEmpty) { + fail('No items found'); + } else if (itemFinder.evaluate().length != items.length) { + fail('Incorrect number of items found'); + } else { + for (int i = 0; i < items.length; i++) { + expect( + tester + .widget(find.descendant(of: itemFinder.at(i), matching: find.byType(Container)).first) + .padding, + EdgeInsets.only( + left: Zeta.of(context).spacing.small, + right: Zeta.of(context).spacing.small, + bottom: Zeta.of(context).spacing.small, + ), + ); + } + } + }); + + testWidgets('the divider is the correct size', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaNavigationBar.divided( + items: items, + dividerIndex: 2, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final dividerFinder = find.byWidgetPredicate( + (widget) => widget is Container && widget.color == Zeta.of(context).colors.borderSubtle, + ); + + expect(dividerFinder, findsOneWidget); + + expect(dividerFinder.evaluate().first.size?.height, Zeta.of(context).spacing.xl_7); + expect(dividerFinder.evaluate().first.size?.width, 1); + }); + }); + + group('Styling Tests', () { + testWidgets('renders the correct background color', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final containerFinder = find.byType(Container).first; + + final containerWidget = tester.widget(containerFinder); + final boxDecoration = containerWidget.decoration! as BoxDecoration; + + expect(boxDecoration.color, Zeta.of(context).colors.surfacePrimary); + }); + + testWidgets('items are the correct color', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final itemFinder = find.byType(NavigationItem); + final icon = + tester.widget(find.descendant(of: itemFinder.first, matching: find.byType(ZetaIcon)).first); + final label = tester.widget(find.descendant(of: itemFinder.first, matching: find.text('Label0'))); + + expect(icon.color, Zeta.of(context).colors.textSubtle); + expect(label.style, Theme.of(context).textTheme.labelSmall?.copyWith(color: Zeta.of(context).colors.textSubtle)); + }); + + testWidgets('selected item is the correct color', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + currentIndex: 0, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final itemFinder = find.byType(NavigationItem); + final icon = + tester.widget(find.descendant(of: itemFinder.first, matching: find.byType(ZetaIcon)).first); + final label = tester.widget(find.descendant(of: itemFinder.first, matching: find.text('Label0'))); + + expect(icon.color, Zeta.of(context).colors.primary); + expect(label.style, Theme.of(context).textTheme.labelSmall?.copyWith(color: Zeta.of(context).colors.primary)); + }); + + testWidgets('hover background color is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + currentIndex: 0, + ), + ), + ); + final context = getBuildContext(tester, ZetaNavigationBar); + + final itemFinder = find.byType(NavigationItem).first; + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: tester.getCenter(itemFinder)); + addTearDown(gesture.removePointer); + + await tester.pumpAndSettle(); + + final inkResponse = + tester.widget(find.descendant(of: itemFinder, matching: find.byType(InkResponse)).first); + + expect(inkResponse.hoverColor, Zeta.of(context).colors.surfaceHover); + }); + }); + + group('Interaction Tests', () { + testWidgets('calls onTap when an item is tapped', (WidgetTester tester) async { + var tappedIndex = -1; + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + onTap: (index) => tappedIndex = index, + ), + ), + ); + + final itemFinder = find.byType(NavigationItem).first; + await tester.tap(itemFinder); + + expect(tappedIndex, 0); + + final lastItemFinder = find.byType(NavigationItem).last; + await tester.tap(lastItemFinder); + + expect(tappedIndex, 3); + }); + + testWidgets('calls onTap when an item is tapped off center', (WidgetTester tester) async { + var tappedIndex = -1; + await tester.pumpWidget( + TestApp( + home: ZetaNavigationBar( + items: items, + onTap: (index) => tappedIndex = index, + ), + ), + ); + + final itemFinder = find.byType(NavigationItem).first; + + await tester.tapAt(tester.getCenter(itemFinder) + const Offset(80, 0)); + expect(tappedIndex, 0); + + final lastItemFinder = find.byType(NavigationItem).last; + + await tester.tapAt(tester.getCenter(lastItemFinder) + const Offset(-80, 0)); + expect(tappedIndex, 3); + }); + + testWidgets('updates the selected item when an item is tapped', (WidgetTester tester) async { + var selectedIndex = -1; + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState) { + return TestApp( + home: ZetaNavigationBar( + items: items, + currentIndex: selectedIndex, + onTap: (val) => setState(() { + selectedIndex = val; + }), + ), + ); + }, + ), + ); + + final itemFinder = find.byType(NavigationItem).first; + await tester.tap(itemFinder); + expect(selectedIndex, 0); + + final lastItemFinder = find.byType(NavigationItem).last; + await tester.tap(lastItemFinder); + expect(selectedIndex, 3); + }); + }); + + group('Golden Tests', () { + goldenTest( + goldenFile, + ZetaNavigationBar(items: items), + 'navigation_bar_default', + ); + goldenTest( + goldenFile, + ZetaNavigationBar( + items: items, + shrinkItems: true, + ), + 'navigation_bar_shrink_items', + ); + for (int i = 0; i < items.length; i++) { + goldenTest( + goldenFile, + ZetaNavigationBar( + items: items, + currentIndex: i, + ), + 'navigation_bar_current_index_$i', + ); + + goldenTest( + goldenFile, + ZetaNavigationBar.divided( + items: items, + dividerIndex: i, + ), + 'navigation_bar_divider_at_$i', + ); + } + goldenTest( + goldenFile, + const ZetaNavigationBar.action( + items: items, + action: ZetaButton(label: 'Button'), + ), + 'navigation_bar_action', + ); + goldenTest( + goldenFile, + const ZetaNavigationBar.split( + items: items, + ), + 'navigation_bar_split', + ); + }); + + group('Performance Tests', () {}); +} diff --git a/test/src/components/stepper/stepper_test.dart b/test/src/components/stepper/stepper_test.dart index 8dd9a0bb..53d4734e 100644 --- a/test/src/components/stepper/stepper_test.dart +++ b/test/src/components/stepper/stepper_test.dart @@ -15,7 +15,7 @@ void main() { goldenFileComparator = TolerantComparator(goldenFile.uri); }); - group('ZetaStepper Accessibility Tests', () { + group('Accessibility Tests', () { testWidgets('Horizontal stepper meets accessibility requirements', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( @@ -91,7 +91,7 @@ void main() { }); }); - group('ZetaStepper Content Tests', () { + group('Content Tests', () { testWidgets('Horizontal stepper renders the correct steps', (WidgetTester tester) async { await tester.pumpWidget( TestApp( @@ -202,7 +202,7 @@ void main() { ); }); - group('ZetaStepper Dimensions Tests', () { + group('Dimensions Tests', () { testWidgets('StepIcon horiztonal has the correct size', (WidgetTester tester) async { await tester.pumpWidget( const TestApp( @@ -275,7 +275,7 @@ void main() { }); }); - group('ZetaStepper Styling Tests', () { + group('Styling Tests', () { testWidgets( 'StepIcon has the correct colour when enabled', (WidgetTester tester) async { @@ -401,7 +401,7 @@ void main() { ); }); - group('ZetaStepper Interaction Tests', () { + group('Interaction Tests', () { testWidgets('Horizontal stepper calls onStepTapped when a step is tapped', (WidgetTester tester) async { int tappedStep = -1; await tester.pumpWidget( @@ -448,7 +448,7 @@ void main() { }); }); - group('ZetaStepper Golden Tests', () { + group('Golden Tests', () { goldenTest( goldenFile, const ZetaStepper( diff --git a/test/src/components/top_app_bar/top_app_bar_test.dart b/test/src/components/top_app_bar/top_app_bar_test.dart index 8ae60e83..921ab1cb 100644 --- a/test/src/components/top_app_bar/top_app_bar_test.dart +++ b/test/src/components/top_app_bar/top_app_bar_test.dart @@ -14,7 +14,7 @@ void main() { goldenFileComparator = TolerantComparator(goldenFile.uri); }); - group('ZetaTopAppBar Accessibility Tests', () { + group('Accessibility Tests', () { testWidgets('ZetaTopAppBar meets accessibility requirements', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( @@ -87,7 +87,7 @@ void main() { }); }); - group('ZetaTopAppBar Content Tests', () { + group('Content Tests', () { final debugFillProperties = { 'titleTextStyle': 'null', 'onSearch': 'null', @@ -216,7 +216,7 @@ void main() { }); }); - group('ZetaTopAppBar Dimensions Tests', () { + group('Dimensions Tests', () { testWidgets('ZetaTopAppBar has the correct height', (WidgetTester tester) async { await tester.pumpWidget( const TestApp( @@ -231,7 +231,7 @@ void main() { }); }); - group('ZetaTopAppBar Styling Tests', () { + group('Styling Tests', () { testWidgets('ZetaTopAppBar has the correct background color', (WidgetTester tester) async { await tester.pumpWidget( const TestApp( @@ -247,7 +247,7 @@ void main() { }); }); - group('ZetaTopAppBar Interaction Tests', () { + group('Interaction Tests', () { late ZetaSearchController searchController; const searchLabel = 'Search'; const clearLabel = 'Clear'; @@ -430,7 +430,7 @@ void main() { }); }); - group('ZetaTopAppBar Golden Tests', () { + group('Golden Tests', () { goldenTest( goldenFile, const ZetaTopAppBar( @@ -553,5 +553,5 @@ void main() { ); }); - group('ZetaTopAppBar Performance Tests', () {}); + group('Performance Tests', () {}); }