Skip to content

Commit d8378bd

Browse files
Add accessibility testing requirements and comprehensive semantic tests
Co-authored-by: richardthe3rd <573334+richardthe3rd@users.noreply.github.com>
1 parent 8e73ebe commit d8378bd

2 files changed

Lines changed: 374 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,82 @@ When adding or modifying UI:
422422
- **Android**: TalkBack, Accessibility Scanner app
423423
- **iOS**: VoiceOver, Accessibility Inspector
424424
- **Web**: NVDA, JAWS, ChromeVox, axe DevTools
425-
- **Flutter**: `flutter test --enable-semantics`
425+
- **Flutter**: `flutter test` (semantics are always enabled in tests)
426+
427+
### Automated Accessibility Testing
428+
429+
**REQUIRED**: All new interactive UI elements must have corresponding semantic tests.
430+
431+
When adding or modifying widgets with `Semantics`:
432+
433+
1. **Add tests to verify semantic properties** in the corresponding test file:
434+
```dart
435+
testWidgets('button has correct semantic label', (tester) async {
436+
await tester.pumpWidget(
437+
MaterialApp(
438+
home: Scaffold(
439+
body: MyButton(),
440+
),
441+
),
442+
);
443+
444+
// Find the Semantics widget
445+
final semantics = tester.widget<Semantics>(
446+
find.byType(Semantics).first,
447+
);
448+
449+
// Verify semantic properties
450+
expect(semantics.properties.label, 'Add to favorites');
451+
expect(semantics.properties.hint, 'Double tap to toggle');
452+
expect(semantics.properties.button, isTrue);
453+
});
454+
```
455+
456+
2. **Test semantic values for stateful elements**:
457+
```dart
458+
testWidgets('filter shows selection state in semantics', (tester) async {
459+
await tester.pumpWidget(
460+
MaterialApp(
461+
home: Scaffold(
462+
body: FilterChip(
463+
label: Text('IPA'),
464+
selected: true,
465+
),
466+
),
467+
),
468+
);
469+
470+
final semantics = tester.widget<Semantics>(
471+
find.byType(Semantics).first,
472+
);
473+
474+
expect(semantics.properties.value, 'Selected');
475+
expect(semantics.properties.selected, isTrue);
476+
});
477+
```
478+
479+
3. **Verify ExcludeSemantics for decorative elements**:
480+
```dart
481+
testWidgets('decorative icon is excluded from semantics', (tester) async {
482+
await tester.pumpWidget(
483+
MaterialApp(
484+
home: Scaffold(
485+
body: ExcludeSemantics(
486+
child: Icon(Icons.percent),
487+
),
488+
),
489+
),
490+
);
491+
492+
// Verify the ExcludeSemantics wrapper exists
493+
expect(find.byType(ExcludeSemantics), findsOneWidget);
494+
});
495+
```
496+
497+
**Test file locations** (mirror lib/ structure):
498+
- `lib/widgets/drink_card.dart``test/drink_card_test.dart`
499+
- `lib/screens/drinks_screen.dart` → Tests may be split into multiple files
500+
- Add semantic tests to existing test files where applicable
426501

427502
## Working with Models
428503

test/accessibility_test.dart

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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

Comments
 (0)