1+ import 'package:flutter/material.dart' ;
2+ import 'package:flutter_test/flutter_test.dart' ;
3+ import 'package:cambridge_beer_festival/models/models.dart' ;
4+ import 'package:cambridge_beer_festival/widgets/widgets.dart' ;
5+
6+ void main () {
7+ group ('Accessibility - DrinkCard Semantics' , () {
8+ testWidgets ('favorite button exists and is interactive' , (tester) async {
9+ final drink = Drink (
10+ product: Product (
11+ id: '1' ,
12+ name: 'Test Beer' ,
13+ abv: 5.0 ,
14+ category: 'beer' ,
15+ dispense: 'cask' ,
16+ ),
17+ producer: Producer (
18+ id: 'p1' ,
19+ name: 'Test Brewery' ,
20+ location: 'Cambridge' ,
21+ products: [],
22+ ),
23+ festivalId: 'test2025' ,
24+ );
25+
26+ await tester.pumpWidget (
27+ MaterialApp (
28+ home: Scaffold (
29+ body: DrinkCard (
30+ drink: drink,
31+ onFavoriteTap: () {},
32+ ),
33+ ),
34+ ),
35+ );
36+
37+ // Verify favorite button exists
38+ expect (find.byType (IconButton ), findsWidgets,
39+ reason: 'DrinkCard should have interactive buttons' );
40+
41+ // Verify Semantics widgets are present
42+ expect (find.byType (Semantics ), findsWidgets,
43+ reason: 'DrinkCard should have semantic labels' );
44+ });
45+
46+ testWidgets ('decorative ABV chip is excluded from semantics' , (tester) async {
47+ final drink = Drink (
48+ product: Product (
49+ id: '1' ,
50+ name: 'Test Beer' ,
51+ abv: 5.0 ,
52+ category: 'beer' ,
53+ dispense: 'cask' ,
54+ ),
55+ producer: Producer (
56+ id: 'p1' ,
57+ name: 'Test Brewery' ,
58+ location: 'Cambridge' ,
59+ products: [],
60+ ),
61+ festivalId: 'test2025' ,
62+ );
63+
64+ await tester.pumpWidget (
65+ MaterialApp (
66+ home: Scaffold (
67+ body: DrinkCard (drink: drink),
68+ ),
69+ ),
70+ );
71+
72+ // Verify ExcludeSemantics is used for decorative info chips
73+ expect (find.byType (ExcludeSemantics ), findsWidgets,
74+ reason: 'Decorative elements should use ExcludeSemantics' );
75+ });
76+
77+ testWidgets ('card has semantic structure' , (tester) async {
78+ final drink = Drink (
79+ product: Product (
80+ id: '1' ,
81+ name: 'Test IPA' ,
82+ abv: 6.5 ,
83+ category: 'beer' ,
84+ dispense: 'cask' ,
85+ style: 'IPA' ,
86+ ),
87+ producer: Producer (
88+ id: 'p1' ,
89+ name: 'Test Brewery' ,
90+ location: 'Cambridge' ,
91+ products: [],
92+ ),
93+ festivalId: 'test2025' ,
94+ );
95+
96+ await tester.pumpWidget (
97+ MaterialApp (
98+ home: Scaffold (
99+ body: DrinkCard (drink: drink),
100+ ),
101+ ),
102+ );
103+
104+ // Verify Card and Semantics widgets exist
105+ expect (find.byType (Card ), findsOneWidget,
106+ reason: 'DrinkCard should use Card widget' );
107+ expect (find.byType (Semantics ), findsWidgets,
108+ reason: 'DrinkCard should have semantic structure' );
109+ });
110+ });
111+
112+ group ('Accessibility - EnvironmentBadge Semantics' , () {
113+ testWidgets ('environment badge renders with semantic labels' , (tester) async {
114+ await tester.pumpWidget (
115+ MaterialApp (
116+ home: Scaffold (
117+ body: Stack (
118+ children: const [
119+ EnvironmentBadge (environmentName: 'staging' ),
120+ ],
121+ ),
122+ ),
123+ ),
124+ );
125+
126+ await tester.pumpAndSettle ();
127+
128+ // Verify badge renders
129+ expect (find.byType (EnvironmentBadge ), findsOneWidget,
130+ reason: 'Environment badge should render' );
131+
132+ // Verify it has Semantics
133+ expect (find.byType (Semantics ), findsWidgets,
134+ reason: 'Environment badge should have semantic labels' );
135+ });
136+ });
137+
138+ group ('Accessibility - Semantic Button Patterns' , () {
139+ testWidgets ('buttons use button property in Semantics' , (tester) async {
140+ await tester.pumpWidget (
141+ MaterialApp (
142+ home: Scaffold (
143+ body: Semantics (
144+ label: 'Test button' ,
145+ button: true ,
146+ child: ElevatedButton (
147+ onPressed: () {},
148+ child: const Text ('Click me' ),
149+ ),
150+ ),
151+ ),
152+ ),
153+ );
154+
155+ // Find our custom Semantics wrapper
156+ final allSemantics = tester.widgetList <Semantics >(
157+ find.byType (Semantics ),
158+ ).toList ();
159+
160+ // Find the one with our specific label
161+ final ourSemantics = allSemantics.firstWhere (
162+ (s) => s.properties.label == 'Test button' ,
163+ );
164+
165+ expect (ourSemantics.properties.button, isTrue,
166+ reason: 'Interactive buttons must set button: true in Semantics' );
167+ });
168+
169+ testWidgets ('hints provide usage instructions when present' , (tester) async {
170+ await tester.pumpWidget (
171+ MaterialApp (
172+ home: Scaffold (
173+ body: Semantics (
174+ label: 'Add to favorites' ,
175+ hint: 'Double tap to toggle' ,
176+ button: true ,
177+ child: IconButton (
178+ icon: const Icon (Icons .favorite_border),
179+ onPressed: () {},
180+ ),
181+ ),
182+ ),
183+ ),
184+ );
185+
186+ // Find our custom Semantics wrapper
187+ final allSemantics = tester.widgetList <Semantics >(
188+ find.byType (Semantics ),
189+ ).toList ();
190+
191+ // Find the one with our specific label
192+ final ourSemantics = allSemantics.firstWhere (
193+ (s) => s.properties.label == 'Add to favorites' ,
194+ );
195+
196+ expect (ourSemantics.properties.hint, 'Double tap to toggle' ,
197+ reason: 'Hint should match expected instruction' );
198+ });
199+
200+ testWidgets ('ExcludeSemantics is used for decorative elements' , (tester) async {
201+ await tester.pumpWidget (
202+ MaterialApp (
203+ home: Scaffold (
204+ body: Column (
205+ children: [
206+ const Text ('Content' ),
207+ ExcludeSemantics (
208+ child: Container (
209+ width: 50 ,
210+ height: 50 ,
211+ color: Colors .blue,
212+ ),
213+ ),
214+ ],
215+ ),
216+ ),
217+ ),
218+ );
219+
220+ expect (find.byType (ExcludeSemantics ), findsAtLeastNWidgets (1 ),
221+ reason: 'Decorative elements should be wrapped in ExcludeSemantics' );
222+ });
223+ });
224+
225+ group ('Accessibility - Semantic State Communication' , () {
226+ testWidgets ('filter selection state is communicated via semantics' , (tester) async {
227+ var isSelected = true ;
228+
229+ await tester.pumpWidget (
230+ MaterialApp (
231+ home: Scaffold (
232+ body: Semantics (
233+ label: 'Filter by IPA' ,
234+ value: isSelected ? 'Selected' : 'Not selected' ,
235+ selected: isSelected,
236+ button: true ,
237+ child: FilterChip (
238+ label: const Text ('IPA' ),
239+ selected: isSelected,
240+ onSelected: (value) => isSelected = value,
241+ ),
242+ ),
243+ ),
244+ ),
245+ );
246+
247+ // Find our custom Semantics wrapper (not the ones Material adds)
248+ final allSemantics = tester.widgetList <Semantics >(
249+ find.byType (Semantics ),
250+ ).toList ();
251+
252+ // Find the one with our specific label
253+ final ourSemantics = allSemantics.firstWhere (
254+ (s) => s.properties.label == 'Filter by IPA' ,
255+ );
256+
257+ expect (ourSemantics.properties.value, 'Selected' ,
258+ reason: 'Selected state should be communicated via value property' );
259+ expect (ourSemantics.properties.selected, isTrue,
260+ reason: 'Selected property should be set to true' );
261+ });
262+
263+ testWidgets ('retry button has descriptive label and hint' , (tester) async {
264+ await tester.pumpWidget (
265+ MaterialApp (
266+ home: Scaffold (
267+ body: Semantics (
268+ label: 'Retry loading drinks' ,
269+ hint: 'Double tap to reload festival data' ,
270+ button: true ,
271+ child: ElevatedButton (
272+ onPressed: () {},
273+ child: const Text ('Retry' ),
274+ ),
275+ ),
276+ ),
277+ ),
278+ );
279+
280+ // Find our custom Semantics wrapper
281+ final allSemantics = tester.widgetList <Semantics >(
282+ find.byType (Semantics ),
283+ ).toList ();
284+
285+ // Find the one with our specific label
286+ final ourSemantics = allSemantics.firstWhere (
287+ (s) => s.properties.label == 'Retry loading drinks' ,
288+ );
289+
290+ expect (ourSemantics.properties.label, 'Retry loading drinks' ,
291+ reason: 'Retry button should clearly state what will be retried' );
292+ expect (ourSemantics.properties.hint, contains ('Double tap' ),
293+ reason: 'Hint should explain the interaction method' );
294+ expect (ourSemantics.properties.button, isTrue,
295+ reason: 'Button property must be set' );
296+ });
297+ });
298+ }
0 commit comments