@@ -5,6 +5,7 @@ import 'package:cambridge_beer_festival/screens/screens.dart';
55import 'package:cambridge_beer_festival/models/models.dart' ;
66import 'package:cambridge_beer_festival/providers/providers.dart' ;
77import 'package:cambridge_beer_festival/services/services.dart' ;
8+ import 'package:cambridge_beer_festival/widgets/widgets.dart' ;
89import 'package:go_router/go_router.dart' ;
910import 'package:provider/provider.dart' ;
1011import 'package:mockito/mockito.dart' ;
@@ -637,4 +638,148 @@ void main() {
637638 provider.dispose ();
638639 });
639640 });
641+
642+ // The filter bar shows formatted category names ('International Beer'), so
643+ // the screen reader must announce those same names rather than the raw
644+ // category ids used as map keys ('international-beer').
645+ group ('DrinksScreen category filter button label' , () {
646+ late MockDrinkRepository mockDrinkRepository;
647+ late MockFestivalRepository mockFestivalRepository;
648+ late MockAnalyticsService mockAnalyticsService;
649+ late BeerProvider provider;
650+
651+ const testFestival = Festival (
652+ id: 'cbf2025' ,
653+ name: 'Cambridge Beer Festival 2025' ,
654+ dataBaseUrl: 'https://test.example.com/cbf2025' ,
655+ );
656+
657+ Drink drinkIn (String category, String id) => Drink (
658+ product: Product (
659+ id: id,
660+ name: 'Drink $id ' ,
661+ abv: 5 ,
662+ category: category,
663+ dispense: 'cask' ,
664+ ),
665+ producer: const Producer (
666+ id: 'brewery1' ,
667+ name: 'Test Brewery' ,
668+ location: 'Cambridge' ,
669+ products: [],
670+ ),
671+ festivalId: 'cbf2025' ,
672+ );
673+
674+ setUp (() async {
675+ SharedPreferences .setMockInitialValues ({});
676+ mockDrinkRepository = MockDrinkRepository ();
677+ mockFestivalRepository = MockFestivalRepository ();
678+ mockAnalyticsService = MockAnalyticsService ();
679+
680+ when (mockFestivalRepository.getFestivals ()).thenAnswer (
681+ (_) async => FestivalsResponse (
682+ festivals: [testFestival],
683+ defaultFestivalId: 'cbf2025' ,
684+ baseUrl: 'https://example.com' ,
685+ version: '1.0.0' ,
686+ ),
687+ );
688+ when (
689+ mockFestivalRepository.getSelectedFestivalId (),
690+ ).thenAnswer ((_) async => null );
691+ when (mockDrinkRepository.getDrinks (any)).thenAnswer (
692+ (_) async => [
693+ drinkIn ('international-beer' , 'd1' ),
694+ drinkIn ('cider' , 'd2' ),
695+ ],
696+ );
697+
698+ provider = BeerProvider (
699+ drinkRepository: mockDrinkRepository,
700+ festivalRepository: mockFestivalRepository,
701+ analyticsService: mockAnalyticsService,
702+ );
703+ await provider.initialize ();
704+ await provider.loadDrinks ();
705+ });
706+
707+ tearDown (() {
708+ provider.dispose ();
709+ });
710+
711+ // Drink cards render their own category chip, so the category name can
712+ // appear more than once on screen. Scope assertions to the filter bar.
713+ Finder buttonText (String text) => find.descendant (
714+ of: find.byType (FilterButton ),
715+ matching: find.text (text),
716+ );
717+
718+ Future <void > pumpScreen (WidgetTester tester) async {
719+ await tester.pumpWidget (
720+ ChangeNotifierProvider <BeerProvider >.value (
721+ value: provider,
722+ child: const MaterialApp (home: DrinksScreen (festivalId: 'cbf2025' )),
723+ ),
724+ );
725+ await tester.pumpAndSettle ();
726+ }
727+
728+ testWidgets ('reads "Category" with no selection' , (tester) async {
729+ await pumpScreen (tester);
730+
731+ expect (buttonText ('Category' ), findsOneWidget);
732+ expect (find.bySemanticsLabel ('Filter by category' ), findsOneWidget);
733+ });
734+
735+ testWidgets ('announces the formatted name for a single selection' , (
736+ tester,
737+ ) async {
738+ await pumpScreen (tester);
739+ provider.toggleCategory ('international-beer' );
740+ await tester.pumpAndSettle ();
741+
742+ expect (buttonText ('International Beer' ), findsOneWidget);
743+ expect (
744+ find.bySemanticsLabel ('Filter by category: International Beer' ),
745+ findsOneWidget,
746+ );
747+ // The raw id must not reach the user, visibly or audibly.
748+ expect (buttonText ('international-beer' ), findsNothing);
749+ expect (
750+ find.bySemanticsLabel ('Filter by category: international-beer' ),
751+ findsNothing,
752+ );
753+ });
754+
755+ testWidgets ('announces every formatted name, sorted, for a multi '
756+ 'selection while the visible label counts them' , (tester) async {
757+ await pumpScreen (tester);
758+ provider
759+ ..toggleCategory ('international-beer' )
760+ ..toggleCategory ('cider' );
761+ await tester.pumpAndSettle ();
762+
763+ expect (buttonText ('2 categories' ), findsOneWidget);
764+ expect (
765+ find.bySemanticsLabel ('Filter by category: Cider, International Beer' ),
766+ findsOneWidget,
767+ );
768+ });
769+
770+ testWidgets ('returns to "Category" once the selection is cleared' , (
771+ tester,
772+ ) async {
773+ await pumpScreen (tester);
774+ provider.toggleCategory ('cider' );
775+ await tester.pumpAndSettle ();
776+ expect (buttonText ('Cider' ), findsOneWidget);
777+
778+ provider.clearCategories ();
779+ await tester.pumpAndSettle ();
780+
781+ expect (buttonText ('Category' ), findsOneWidget);
782+ expect (find.bySemanticsLabel ('Filter by category' ), findsOneWidget);
783+ });
784+ });
640785}
0 commit comments