1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/cupertino.dart'; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter/services.dart'; |
10 | import 'package:flutter_test/flutter_test.dart'; |
11 | |
12 | import '../widgets/feedback_tester.dart'; |
13 | import '../widgets/semantics_tester.dart'; |
14 | |
15 | Widget wrap({Widget? child}) { |
16 | return MediaQuery( |
17 | data: const MediaQueryData(), |
18 | child: Directionality(textDirection: TextDirection.ltr, child: Material(child: child)), |
19 | ); |
20 | } |
21 | |
22 | void main() { |
23 | testWidgets('RadioListTile should initialize according to groupValue' , ( |
24 | WidgetTester tester, |
25 | ) async { |
26 | final List<int> values = <int>[0, 1, 2]; |
27 | int? selectedValue; |
28 | // Constructor parameters are required for [RadioListTile], but they are |
29 | // irrelevant when searching with [find.byType]. |
30 | final Type radioListTileType = |
31 | const RadioListTile<int>(value: 0, groupValue: 0, onChanged: null).runtimeType; |
32 | |
33 | List<RadioListTile<int>> generatedRadioListTiles; |
34 | List<RadioListTile<int>> findTiles() => |
35 | find |
36 | .byType(radioListTileType) |
37 | .evaluate() |
38 | .map<Widget>((Element element) => element.widget) |
39 | .cast<RadioListTile<int>>() |
40 | .toList(); |
41 | |
42 | Widget buildFrame() { |
43 | return wrap( |
44 | child: StatefulBuilder( |
45 | builder: (BuildContext context, StateSetter setState) { |
46 | return Scaffold( |
47 | body: ListView.builder( |
48 | itemCount: values.length, |
49 | itemBuilder: |
50 | (BuildContext context, int index) => RadioListTile<int>( |
51 | onChanged: (int? value) { |
52 | setState(() { |
53 | selectedValue = value; |
54 | }); |
55 | }, |
56 | value: values[index], |
57 | groupValue: selectedValue, |
58 | title: Text(values[index].toString()), |
59 | ), |
60 | ), |
61 | ); |
62 | }, |
63 | ), |
64 | ); |
65 | } |
66 | |
67 | await tester.pumpWidget(buildFrame()); |
68 | generatedRadioListTiles = findTiles(); |
69 | |
70 | expect(generatedRadioListTiles[0].checked, equals(false)); |
71 | expect(generatedRadioListTiles[1].checked, equals(false)); |
72 | expect(generatedRadioListTiles[2].checked, equals(false)); |
73 | |
74 | selectedValue = 1; |
75 | |
76 | await tester.pumpWidget(buildFrame()); |
77 | generatedRadioListTiles = findTiles(); |
78 | |
79 | expect(generatedRadioListTiles[0].checked, equals(false)); |
80 | expect(generatedRadioListTiles[1].checked, equals(true)); |
81 | expect(generatedRadioListTiles[2].checked, equals(false)); |
82 | }); |
83 | |
84 | testWidgets('RadioListTile simple control test' , (WidgetTester tester) async { |
85 | final Key key = UniqueKey(); |
86 | final Key titleKey = UniqueKey(); |
87 | final List<int?> log = <int?>[]; |
88 | |
89 | await tester.pumpWidget( |
90 | wrap( |
91 | child: RadioListTile<int>( |
92 | key: key, |
93 | value: 1, |
94 | groupValue: 2, |
95 | onChanged: log.add, |
96 | title: Text('Title' , key: titleKey), |
97 | ), |
98 | ), |
99 | ); |
100 | |
101 | await tester.tap(find.byKey(key)); |
102 | |
103 | expect(log, equals(<int>[1])); |
104 | log.clear(); |
105 | |
106 | await tester.pumpWidget( |
107 | wrap( |
108 | child: RadioListTile<int>( |
109 | key: key, |
110 | value: 1, |
111 | groupValue: 1, |
112 | onChanged: log.add, |
113 | activeColor: Colors.green[500], |
114 | title: Text('Title' , key: titleKey), |
115 | ), |
116 | ), |
117 | ); |
118 | |
119 | await tester.tap(find.byKey(key)); |
120 | |
121 | expect(log, isEmpty); |
122 | |
123 | await tester.pumpWidget( |
124 | wrap( |
125 | child: RadioListTile<int>( |
126 | key: key, |
127 | value: 1, |
128 | groupValue: 2, |
129 | onChanged: null, |
130 | title: Text('Title' , key: titleKey), |
131 | ), |
132 | ), |
133 | ); |
134 | |
135 | await tester.tap(find.byKey(key)); |
136 | |
137 | expect(log, isEmpty); |
138 | |
139 | await tester.pumpWidget( |
140 | wrap( |
141 | child: RadioListTile<int>( |
142 | key: key, |
143 | value: 1, |
144 | groupValue: 2, |
145 | onChanged: log.add, |
146 | title: Text('Title' , key: titleKey), |
147 | ), |
148 | ), |
149 | ); |
150 | |
151 | await tester.tap(find.byKey(titleKey)); |
152 | |
153 | expect(log, equals(<int>[1])); |
154 | }); |
155 | |
156 | testWidgets('RadioListTile control tests' , (WidgetTester tester) async { |
157 | final List<int> values = <int>[0, 1, 2]; |
158 | int? selectedValue; |
159 | // Constructor parameters are required for [Radio], but they are irrelevant |
160 | // when searching with [find.byType]. |
161 | final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType; |
162 | final List<dynamic> log = <dynamic>[]; |
163 | |
164 | Widget buildFrame() { |
165 | return wrap( |
166 | child: StatefulBuilder( |
167 | builder: (BuildContext context, StateSetter setState) { |
168 | return Scaffold( |
169 | body: ListView.builder( |
170 | itemCount: values.length, |
171 | itemBuilder: |
172 | (BuildContext context, int index) => RadioListTile<int>( |
173 | onChanged: (int? value) { |
174 | log.add(value); |
175 | setState(() { |
176 | selectedValue = value; |
177 | }); |
178 | }, |
179 | value: values[index], |
180 | groupValue: selectedValue, |
181 | title: Text(values[index].toString()), |
182 | ), |
183 | ), |
184 | ); |
185 | }, |
186 | ), |
187 | ); |
188 | } |
189 | |
190 | // Tests for tapping between [Radio] and [ListTile] |
191 | await tester.pumpWidget(buildFrame()); |
192 | await tester.tap(find.text('1' )); |
193 | log.add('-' ); |
194 | await tester.tap(find.byType(radioType).at(2)); |
195 | expect(log, equals(<dynamic>[1, '-' , 2])); |
196 | log.add('-' ); |
197 | await tester.tap(find.text('1' )); |
198 | |
199 | log.clear(); |
200 | selectedValue = null; |
201 | |
202 | // Tests for tapping across [Radio]s exclusively |
203 | await tester.pumpWidget(buildFrame()); |
204 | await tester.tap(find.byType(radioType).at(1)); |
205 | log.add('-' ); |
206 | await tester.tap(find.byType(radioType).at(2)); |
207 | expect(log, equals(<dynamic>[1, '-' , 2])); |
208 | |
209 | log.clear(); |
210 | selectedValue = null; |
211 | |
212 | // Tests for tapping across [ListTile]s exclusively |
213 | await tester.pumpWidget(buildFrame()); |
214 | await tester.tap(find.text('1' )); |
215 | log.add('-' ); |
216 | await tester.tap(find.text('2' )); |
217 | expect(log, equals(<dynamic>[1, '-' , 2])); |
218 | }); |
219 | |
220 | testWidgets('Selected RadioListTile should not trigger onChanged' , (WidgetTester tester) async { |
221 | // Regression test for https://github.com/flutter/flutter/issues/30311 |
222 | final List<int> values = <int>[0, 1, 2]; |
223 | int? selectedValue; |
224 | // Constructor parameters are required for [Radio], but they are irrelevant |
225 | // when searching with [find.byType]. |
226 | final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType; |
227 | final List<dynamic> log = <dynamic>[]; |
228 | |
229 | Widget buildFrame() { |
230 | return wrap( |
231 | child: StatefulBuilder( |
232 | builder: (BuildContext context, StateSetter setState) { |
233 | return Scaffold( |
234 | body: ListView.builder( |
235 | itemCount: values.length, |
236 | itemBuilder: |
237 | (BuildContext context, int index) => RadioListTile<int>( |
238 | onChanged: (int? value) { |
239 | log.add(value); |
240 | setState(() { |
241 | selectedValue = value; |
242 | }); |
243 | }, |
244 | value: values[index], |
245 | groupValue: selectedValue, |
246 | title: Text(values[index].toString()), |
247 | ), |
248 | ), |
249 | ); |
250 | }, |
251 | ), |
252 | ); |
253 | } |
254 | |
255 | await tester.pumpWidget(buildFrame()); |
256 | await tester.tap(find.text('0' )); |
257 | await tester.pump(); |
258 | expect(log, equals(<int>[0])); |
259 | |
260 | await tester.tap(find.text('0' )); |
261 | await tester.pump(); |
262 | expect(log, equals(<int>[0])); |
263 | |
264 | await tester.tap(find.byType(radioType).at(0)); |
265 | await tester.pump(); |
266 | expect(log, equals(<int>[0])); |
267 | }); |
268 | |
269 | testWidgets('Selected RadioListTile should trigger onChanged when toggleable' , ( |
270 | WidgetTester tester, |
271 | ) async { |
272 | final List<int> values = <int>[0, 1, 2]; |
273 | int? selectedValue; |
274 | // Constructor parameters are required for [Radio], but they are irrelevant |
275 | // when searching with [find.byType]. |
276 | final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType; |
277 | final List<dynamic> log = <dynamic>[]; |
278 | |
279 | Widget buildFrame() { |
280 | return wrap( |
281 | child: StatefulBuilder( |
282 | builder: (BuildContext context, StateSetter setState) { |
283 | return Scaffold( |
284 | body: ListView.builder( |
285 | itemCount: values.length, |
286 | itemBuilder: (BuildContext context, int index) { |
287 | return RadioListTile<int>( |
288 | onChanged: (int? value) { |
289 | log.add(value); |
290 | setState(() { |
291 | selectedValue = value; |
292 | }); |
293 | }, |
294 | toggleable: true, |
295 | value: values[index], |
296 | groupValue: selectedValue, |
297 | title: Text(values[index].toString()), |
298 | ); |
299 | }, |
300 | ), |
301 | ); |
302 | }, |
303 | ), |
304 | ); |
305 | } |
306 | |
307 | await tester.pumpWidget(buildFrame()); |
308 | await tester.tap(find.text('0' )); |
309 | await tester.pump(); |
310 | expect(log, equals(<int>[0])); |
311 | |
312 | await tester.tap(find.text('0' )); |
313 | await tester.pump(); |
314 | expect(log, equals(<int?>[0, null])); |
315 | |
316 | await tester.tap(find.byType(radioType).at(0)); |
317 | await tester.pump(); |
318 | expect(log, equals(<int?>[0, null, 0])); |
319 | }); |
320 | |
321 | testWidgets('RadioListTile can be toggled when toggleable is set' , (WidgetTester tester) async { |
322 | final Key key = UniqueKey(); |
323 | final List<int?> log = <int?>[]; |
324 | |
325 | await tester.pumpWidget( |
326 | Material( |
327 | child: Center( |
328 | child: Radio<int>( |
329 | key: key, |
330 | value: 1, |
331 | groupValue: 2, |
332 | onChanged: log.add, |
333 | toggleable: true, |
334 | ), |
335 | ), |
336 | ), |
337 | ); |
338 | |
339 | await tester.tap(find.byKey(key)); |
340 | |
341 | expect(log, equals(<int>[1])); |
342 | log.clear(); |
343 | |
344 | await tester.pumpWidget( |
345 | Material( |
346 | child: Center( |
347 | child: Radio<int>( |
348 | key: key, |
349 | value: 1, |
350 | groupValue: 1, |
351 | onChanged: log.add, |
352 | toggleable: true, |
353 | ), |
354 | ), |
355 | ), |
356 | ); |
357 | |
358 | await tester.tap(find.byKey(key)); |
359 | |
360 | expect(log, equals(<int?>[null])); |
361 | log.clear(); |
362 | |
363 | await tester.pumpWidget( |
364 | Material( |
365 | child: Center( |
366 | child: Radio<int>( |
367 | key: key, |
368 | value: 1, |
369 | groupValue: null, |
370 | onChanged: log.add, |
371 | toggleable: true, |
372 | ), |
373 | ), |
374 | ), |
375 | ); |
376 | |
377 | await tester.tap(find.byKey(key)); |
378 | |
379 | expect(log, equals(<int>[1])); |
380 | }); |
381 | |
382 | testWidgets('RadioListTile semantics' , (WidgetTester tester) async { |
383 | final SemanticsTester semantics = SemanticsTester(tester); |
384 | |
385 | await tester.pumpWidget( |
386 | wrap( |
387 | child: RadioListTile<int>( |
388 | value: 1, |
389 | groupValue: 2, |
390 | onChanged: (int? i) {}, |
391 | title: const Text('Title' ), |
392 | internalAddSemanticForOnTap: true, |
393 | ), |
394 | ), |
395 | ); |
396 | |
397 | expect( |
398 | semantics, |
399 | hasSemantics( |
400 | TestSemantics.root( |
401 | children: <TestSemantics>[ |
402 | TestSemantics( |
403 | id: 1, |
404 | flags: <SemanticsFlag>[ |
405 | SemanticsFlag.isButton, |
406 | SemanticsFlag.hasCheckedState, |
407 | SemanticsFlag.hasEnabledState, |
408 | SemanticsFlag.isEnabled, |
409 | SemanticsFlag.isInMutuallyExclusiveGroup, |
410 | SemanticsFlag.isFocusable, |
411 | SemanticsFlag.hasSelectedState, |
412 | ], |
413 | actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
414 | label: 'Title' , |
415 | textDirection: TextDirection.ltr, |
416 | ), |
417 | ], |
418 | ), |
419 | ignoreRect: true, |
420 | ignoreTransform: true, |
421 | ), |
422 | ); |
423 | |
424 | await tester.pumpWidget( |
425 | wrap( |
426 | child: RadioListTile<int>( |
427 | value: 2, |
428 | groupValue: 2, |
429 | onChanged: (int? i) {}, |
430 | title: const Text('Title' ), |
431 | internalAddSemanticForOnTap: true, |
432 | ), |
433 | ), |
434 | ); |
435 | |
436 | expect( |
437 | semantics, |
438 | hasSemantics( |
439 | TestSemantics.root( |
440 | children: <TestSemantics>[ |
441 | TestSemantics( |
442 | id: 1, |
443 | flags: <SemanticsFlag>[ |
444 | SemanticsFlag.isButton, |
445 | SemanticsFlag.hasCheckedState, |
446 | SemanticsFlag.isChecked, |
447 | SemanticsFlag.hasEnabledState, |
448 | SemanticsFlag.isEnabled, |
449 | SemanticsFlag.isInMutuallyExclusiveGroup, |
450 | SemanticsFlag.isFocusable, |
451 | SemanticsFlag.hasSelectedState, |
452 | ], |
453 | actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
454 | label: 'Title' , |
455 | textDirection: TextDirection.ltr, |
456 | ), |
457 | ], |
458 | ), |
459 | ignoreRect: true, |
460 | ignoreTransform: true, |
461 | ), |
462 | ); |
463 | |
464 | await tester.pumpWidget( |
465 | wrap( |
466 | child: const RadioListTile<int>( |
467 | value: 1, |
468 | groupValue: 2, |
469 | onChanged: null, |
470 | title: Text('Title' ), |
471 | internalAddSemanticForOnTap: true, |
472 | ), |
473 | ), |
474 | ); |
475 | |
476 | expect( |
477 | semantics, |
478 | hasSemantics( |
479 | TestSemantics.root( |
480 | children: <TestSemantics>[ |
481 | TestSemantics( |
482 | id: 1, |
483 | flags: <SemanticsFlag>[ |
484 | SemanticsFlag.hasCheckedState, |
485 | SemanticsFlag.hasEnabledState, |
486 | SemanticsFlag.isInMutuallyExclusiveGroup, |
487 | SemanticsFlag.isFocusable, |
488 | SemanticsFlag.hasSelectedState, |
489 | ], |
490 | actions: <SemanticsAction>[SemanticsAction.focus], |
491 | label: 'Title' , |
492 | textDirection: TextDirection.ltr, |
493 | ), |
494 | ], |
495 | ), |
496 | ignoreId: true, |
497 | ignoreRect: true, |
498 | ignoreTransform: true, |
499 | ), |
500 | ); |
501 | |
502 | await tester.pumpWidget( |
503 | wrap( |
504 | child: const RadioListTile<int>( |
505 | value: 2, |
506 | groupValue: 2, |
507 | onChanged: null, |
508 | title: Text('Title' ), |
509 | internalAddSemanticForOnTap: true, |
510 | ), |
511 | ), |
512 | ); |
513 | |
514 | expect( |
515 | semantics, |
516 | hasSemantics( |
517 | TestSemantics.root( |
518 | children: <TestSemantics>[ |
519 | TestSemantics( |
520 | id: 1, |
521 | flags: <SemanticsFlag>[ |
522 | SemanticsFlag.hasCheckedState, |
523 | SemanticsFlag.isChecked, |
524 | SemanticsFlag.hasEnabledState, |
525 | SemanticsFlag.isInMutuallyExclusiveGroup, |
526 | SemanticsFlag.hasSelectedState, |
527 | ], |
528 | label: 'Title' , |
529 | textDirection: TextDirection.ltr, |
530 | ), |
531 | ], |
532 | ), |
533 | ignoreId: true, |
534 | ignoreRect: true, |
535 | ignoreTransform: true, |
536 | ), |
537 | ); |
538 | |
539 | semantics.dispose(); |
540 | }); |
541 | |
542 | testWidgets('RadioListTile has semantic events' , (WidgetTester tester) async { |
543 | final SemanticsTester semantics = SemanticsTester(tester); |
544 | final Key key = UniqueKey(); |
545 | dynamic semanticEvent; |
546 | int? radioValue = 2; |
547 | tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>( |
548 | SystemChannels.accessibility, |
549 | (dynamic message) async { |
550 | semanticEvent = message; |
551 | }, |
552 | ); |
553 | |
554 | await tester.pumpWidget( |
555 | wrap( |
556 | child: RadioListTile<int>( |
557 | key: key, |
558 | value: 1, |
559 | groupValue: radioValue, |
560 | onChanged: (int? i) { |
561 | radioValue = i; |
562 | }, |
563 | title: const Text('Title' ), |
564 | ), |
565 | ), |
566 | ); |
567 | |
568 | await tester.tap(find.byKey(key)); |
569 | await tester.pump(); |
570 | final RenderObject object = tester.firstRenderObject(find.byKey(key)); |
571 | |
572 | expect(radioValue, 1); |
573 | expect(semanticEvent, <String, dynamic>{ |
574 | 'type' : 'tap' , |
575 | 'nodeId' : object.debugSemantics!.id, |
576 | 'data' : <String, dynamic>{}, |
577 | }); |
578 | expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true); |
579 | |
580 | semantics.dispose(); |
581 | tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>( |
582 | SystemChannels.accessibility, |
583 | null, |
584 | ); |
585 | }); |
586 | |
587 | testWidgets('RadioListTile can autofocus unless disabled.' , (WidgetTester tester) async { |
588 | final GlobalKey childKey = GlobalKey(); |
589 | |
590 | await tester.pumpWidget( |
591 | wrap( |
592 | child: RadioListTile<int>( |
593 | value: 1, |
594 | groupValue: 2, |
595 | onChanged: (_) {}, |
596 | title: Text('Title' , key: childKey), |
597 | autofocus: true, |
598 | ), |
599 | ), |
600 | ); |
601 | |
602 | await tester.pump(); |
603 | expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue); |
604 | |
605 | await tester.pumpWidget( |
606 | wrap( |
607 | child: RadioListTile<int>( |
608 | value: 1, |
609 | groupValue: 2, |
610 | onChanged: null, |
611 | title: Text('Title' , key: childKey), |
612 | autofocus: true, |
613 | ), |
614 | ), |
615 | ); |
616 | |
617 | await tester.pump(); |
618 | expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); |
619 | }); |
620 | |
621 | testWidgets('RadioListTile contentPadding test' , (WidgetTester tester) async { |
622 | final Type radioType = |
623 | const Radio<bool>(groupValue: true, value: true, onChanged: null).runtimeType; |
624 | |
625 | await tester.pumpWidget( |
626 | wrap( |
627 | child: Center( |
628 | child: RadioListTile<bool>( |
629 | groupValue: true, |
630 | value: true, |
631 | title: const Text('Title' ), |
632 | onChanged: (_) {}, |
633 | contentPadding: const EdgeInsets.fromLTRB(8, 10, 15, 20), |
634 | ), |
635 | ), |
636 | ), |
637 | ); |
638 | |
639 | final Rect paddingRect = tester.getRect(find.byType(SafeArea)); |
640 | final Rect radioRect = tester.getRect(find.byType(radioType)); |
641 | final Rect titleRect = tester.getRect(find.text('Title' )); |
642 | |
643 | // Get the taller Rect of the Radio and Text widgets |
644 | final Rect tallerRect = radioRect.height > titleRect.height ? radioRect : titleRect; |
645 | |
646 | // Get the extra height between the tallerRect and ListTile height |
647 | final double extraHeight = 56 - tallerRect.height; |
648 | |
649 | // Check for correct top and bottom padding |
650 | expect(paddingRect.top, tallerRect.top - extraHeight / 2 - 10); //top padding |
651 | expect(paddingRect.bottom, tallerRect.bottom + extraHeight / 2 + 20); //bottom padding |
652 | |
653 | // Check for correct left and right padding |
654 | expect(paddingRect.left, radioRect.left - 8); //left padding |
655 | expect(paddingRect.right, titleRect.right + 15); //right padding |
656 | }); |
657 | |
658 | testWidgets('RadioListTile respects shape' , (WidgetTester tester) async { |
659 | const ShapeBorder shapeBorder = RoundedRectangleBorder( |
660 | borderRadius: BorderRadius.horizontal(right: Radius.circular(100)), |
661 | ); |
662 | |
663 | await tester.pumpWidget( |
664 | const MaterialApp( |
665 | home: Material( |
666 | child: RadioListTile<bool>( |
667 | value: true, |
668 | groupValue: true, |
669 | onChanged: null, |
670 | title: Text('Title' ), |
671 | shape: shapeBorder, |
672 | ), |
673 | ), |
674 | ), |
675 | ); |
676 | |
677 | expect(tester.widget<InkWell>(find.byType(InkWell)).customBorder, shapeBorder); |
678 | }); |
679 | |
680 | testWidgets('RadioListTile respects tileColor' , (WidgetTester tester) async { |
681 | final Color tileColor = Colors.red.shade500; |
682 | |
683 | await tester.pumpWidget( |
684 | wrap( |
685 | child: Center( |
686 | child: RadioListTile<bool>( |
687 | value: false, |
688 | groupValue: true, |
689 | onChanged: null, |
690 | title: const Text('Title' ), |
691 | tileColor: tileColor, |
692 | ), |
693 | ), |
694 | ), |
695 | ); |
696 | |
697 | expect(find.byType(Material), paints..rect(color: tileColor)); |
698 | }); |
699 | |
700 | testWidgets('RadioListTile respects selectedTileColor' , (WidgetTester tester) async { |
701 | final Color selectedTileColor = Colors.green.shade500; |
702 | |
703 | await tester.pumpWidget( |
704 | wrap( |
705 | child: Center( |
706 | child: RadioListTile<bool>( |
707 | value: false, |
708 | groupValue: true, |
709 | onChanged: null, |
710 | title: const Text('Title' ), |
711 | selected: true, |
712 | selectedTileColor: selectedTileColor, |
713 | ), |
714 | ), |
715 | ), |
716 | ); |
717 | |
718 | expect(find.byType(Material), paints..rect(color: selectedTileColor)); |
719 | }); |
720 | |
721 | testWidgets('RadioListTile selected item text Color' , (WidgetTester tester) async { |
722 | // Regression test for https://github.com/flutter/flutter/pull/76906 |
723 | |
724 | const Color activeColor = Color(0xff00ff00); |
725 | |
726 | Widget buildFrame({Color? activeColor, Color? fillColor}) { |
727 | return MaterialApp( |
728 | theme: ThemeData( |
729 | radioTheme: RadioThemeData( |
730 | fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) { |
731 | return states.contains(MaterialState.selected) ? fillColor : null; |
732 | }), |
733 | ), |
734 | ), |
735 | home: Scaffold( |
736 | body: Center( |
737 | child: RadioListTile<bool>( |
738 | activeColor: activeColor, |
739 | selected: true, |
740 | title: const Text('title' ), |
741 | value: false, |
742 | groupValue: true, |
743 | onChanged: (bool? newValue) {}, |
744 | ), |
745 | ), |
746 | ), |
747 | ); |
748 | } |
749 | |
750 | Color? textColor(String text) { |
751 | return tester.renderObject<RenderParagraph>(find.text(text)).text.style?.color; |
752 | } |
753 | |
754 | await tester.pumpWidget(buildFrame(fillColor: activeColor)); |
755 | expect(textColor('title' ), activeColor); |
756 | |
757 | await tester.pumpWidget(buildFrame(activeColor: activeColor)); |
758 | expect(textColor('title' ), activeColor); |
759 | }); |
760 | |
761 | testWidgets('RadioListTile respects visualDensity' , (WidgetTester tester) async { |
762 | const Key key = Key('test' ); |
763 | Future<void> buildTest(VisualDensity visualDensity) async { |
764 | return tester.pumpWidget( |
765 | wrap( |
766 | child: Center( |
767 | child: RadioListTile<bool>( |
768 | key: key, |
769 | value: false, |
770 | groupValue: true, |
771 | onChanged: (bool? value) {}, |
772 | autofocus: true, |
773 | visualDensity: visualDensity, |
774 | ), |
775 | ), |
776 | ), |
777 | ); |
778 | } |
779 | |
780 | await buildTest(VisualDensity.standard); |
781 | final RenderBox box = tester.renderObject(find.byKey(key)); |
782 | await tester.pumpAndSettle(); |
783 | expect(box.size, equals(const Size(800, 56))); |
784 | }); |
785 | |
786 | testWidgets('RadioListTile respects focusNode' , (WidgetTester tester) async { |
787 | final GlobalKey childKey = GlobalKey(); |
788 | await tester.pumpWidget( |
789 | wrap( |
790 | child: Center( |
791 | child: RadioListTile<bool>( |
792 | value: false, |
793 | groupValue: true, |
794 | title: Text('A' , key: childKey), |
795 | onChanged: (bool? value) {}, |
796 | ), |
797 | ), |
798 | ), |
799 | ); |
800 | |
801 | await tester.pump(); |
802 | final FocusNode tileNode = Focus.of(childKey.currentContext!); |
803 | tileNode.requestFocus(); |
804 | await tester.pump(); // Let the focus take effect. |
805 | expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue); |
806 | expect(tileNode.hasPrimaryFocus, isTrue); |
807 | }); |
808 | |
809 | testWidgets('RadioListTile onFocusChange callback' , (WidgetTester tester) async { |
810 | final FocusNode node = FocusNode(debugLabel: 'RadioListTile onFocusChange' ); |
811 | addTearDown(node.dispose); |
812 | |
813 | bool gotFocus = false; |
814 | await tester.pumpWidget( |
815 | MaterialApp( |
816 | home: Material( |
817 | child: RadioListTile<bool>( |
818 | value: true, |
819 | focusNode: node, |
820 | onFocusChange: (bool focused) { |
821 | gotFocus = focused; |
822 | }, |
823 | onChanged: (bool? value) {}, |
824 | groupValue: true, |
825 | ), |
826 | ), |
827 | ), |
828 | ); |
829 | |
830 | node.requestFocus(); |
831 | await tester.pump(); |
832 | expect(gotFocus, isTrue); |
833 | expect(node.hasFocus, isTrue); |
834 | |
835 | node.unfocus(); |
836 | await tester.pump(); |
837 | expect(gotFocus, isFalse); |
838 | expect(node.hasFocus, isFalse); |
839 | }); |
840 | |
841 | testWidgets('Radio changes mouse cursor when hovered' , (WidgetTester tester) async { |
842 | // Test Radio() constructor |
843 | await tester.pumpWidget( |
844 | wrap( |
845 | child: MouseRegion( |
846 | cursor: SystemMouseCursors.forbidden, |
847 | child: RadioListTile<int>( |
848 | mouseCursor: SystemMouseCursors.text, |
849 | value: 1, |
850 | onChanged: (int? v) {}, |
851 | groupValue: 2, |
852 | ), |
853 | ), |
854 | ), |
855 | ); |
856 | |
857 | final TestGesture gesture = await tester.createGesture( |
858 | kind: PointerDeviceKind.mouse, |
859 | pointer: 1, |
860 | ); |
861 | await gesture.addPointer(location: tester.getCenter(find.byType(Radio<int>))); |
862 | |
863 | await tester.pump(); |
864 | |
865 | expect( |
866 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
867 | SystemMouseCursors.text, |
868 | ); |
869 | |
870 | // Test default cursor |
871 | await tester.pumpWidget( |
872 | wrap( |
873 | child: MouseRegion( |
874 | cursor: SystemMouseCursors.forbidden, |
875 | child: RadioListTile<int>(value: 1, onChanged: (int? v) {}, groupValue: 2), |
876 | ), |
877 | ), |
878 | ); |
879 | |
880 | expect( |
881 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
882 | SystemMouseCursors.click, |
883 | ); |
884 | |
885 | // Test default cursor when disabled |
886 | await tester.pumpWidget( |
887 | wrap( |
888 | child: const MouseRegion( |
889 | cursor: SystemMouseCursors.forbidden, |
890 | child: RadioListTile<int>(value: 1, onChanged: null, groupValue: 2), |
891 | ), |
892 | ), |
893 | ); |
894 | |
895 | expect( |
896 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
897 | SystemMouseCursors.basic, |
898 | ); |
899 | }); |
900 | |
901 | testWidgets('RadioListTile respects fillColor in enabled/disabled states' , ( |
902 | WidgetTester tester, |
903 | ) async { |
904 | const Color activeEnabledFillColor = Color(0xFF000001); |
905 | const Color activeDisabledFillColor = Color(0xFF000002); |
906 | const Color inactiveEnabledFillColor = Color(0xFF000003); |
907 | const Color inactiveDisabledFillColor = Color(0xFF000004); |
908 | |
909 | Color getFillColor(Set<MaterialState> states) { |
910 | if (states.contains(MaterialState.disabled)) { |
911 | if (states.contains(MaterialState.selected)) { |
912 | return activeDisabledFillColor; |
913 | } |
914 | return inactiveDisabledFillColor; |
915 | } |
916 | if (states.contains(MaterialState.selected)) { |
917 | return activeEnabledFillColor; |
918 | } |
919 | return inactiveEnabledFillColor; |
920 | } |
921 | |
922 | final MaterialStateProperty<Color> fillColor = MaterialStateColor.resolveWith(getFillColor); |
923 | |
924 | int? groupValue = 0; |
925 | Widget buildApp({required bool enabled}) { |
926 | return wrap( |
927 | child: StatefulBuilder( |
928 | builder: (BuildContext context, StateSetter setState) { |
929 | return RadioListTile<int>( |
930 | value: 0, |
931 | fillColor: fillColor, |
932 | onChanged: |
933 | enabled |
934 | ? (int? newValue) { |
935 | setState(() { |
936 | groupValue = newValue; |
937 | }); |
938 | } |
939 | : null, |
940 | groupValue: groupValue, |
941 | ); |
942 | }, |
943 | ), |
944 | ); |
945 | } |
946 | |
947 | await tester.pumpWidget(buildApp(enabled: true)); |
948 | |
949 | // Selected and enabled. |
950 | await tester.pumpAndSettle(); |
951 | expect( |
952 | Material.of(tester.element(find.byType(Radio<int>))), |
953 | paints |
954 | ..rect() |
955 | ..circle(color: activeEnabledFillColor) |
956 | ..circle(color: activeEnabledFillColor), |
957 | ); |
958 | |
959 | // Check when the radio isn't selected. |
960 | groupValue = 1; |
961 | await tester.pumpWidget(buildApp(enabled: true)); |
962 | await tester.pumpAndSettle(); |
963 | expect( |
964 | Material.of(tester.element(find.byType(Radio<int>))), |
965 | paints |
966 | ..rect() |
967 | ..circle(color: inactiveEnabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0), |
968 | ); |
969 | |
970 | // Check when the radio is selected, but disabled. |
971 | groupValue = 0; |
972 | await tester.pumpWidget(buildApp(enabled: false)); |
973 | await tester.pumpAndSettle(); |
974 | expect( |
975 | Material.of(tester.element(find.byType(Radio<int>))), |
976 | paints |
977 | ..rect() |
978 | ..circle(color: activeDisabledFillColor) |
979 | ..circle(color: activeDisabledFillColor), |
980 | ); |
981 | |
982 | // Check when the radio is unselected and disabled. |
983 | groupValue = 1; |
984 | await tester.pumpWidget(buildApp(enabled: false)); |
985 | await tester.pumpAndSettle(); |
986 | expect( |
987 | Material.of(tester.element(find.byType(Radio<int>))), |
988 | paints |
989 | ..rect() |
990 | ..circle(color: inactiveDisabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0), |
991 | ); |
992 | }); |
993 | |
994 | testWidgets('RadioListTile respects fillColor in hovered state' , (WidgetTester tester) async { |
995 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
996 | const Color hoveredFillColor = Color(0xFF000001); |
997 | |
998 | Color getFillColor(Set<MaterialState> states) { |
999 | if (states.contains(MaterialState.hovered)) { |
1000 | return hoveredFillColor; |
1001 | } |
1002 | return Colors.transparent; |
1003 | } |
1004 | |
1005 | final MaterialStateProperty<Color> fillColor = MaterialStateColor.resolveWith(getFillColor); |
1006 | |
1007 | int? groupValue = 0; |
1008 | Widget buildApp() { |
1009 | return wrap( |
1010 | child: StatefulBuilder( |
1011 | builder: (BuildContext context, StateSetter setState) { |
1012 | return RadioListTile<int>( |
1013 | value: 0, |
1014 | fillColor: fillColor, |
1015 | onChanged: (int? newValue) { |
1016 | setState(() { |
1017 | groupValue = newValue; |
1018 | }); |
1019 | }, |
1020 | groupValue: groupValue, |
1021 | ); |
1022 | }, |
1023 | ), |
1024 | ); |
1025 | } |
1026 | |
1027 | await tester.pumpWidget(buildApp()); |
1028 | await tester.pumpAndSettle(); |
1029 | |
1030 | // Start hovering |
1031 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1032 | await gesture.addPointer(); |
1033 | await gesture.moveTo(tester.getCenter(find.byType(Radio<int>))); |
1034 | await tester.pumpAndSettle(); |
1035 | |
1036 | expect( |
1037 | Material.of(tester.element(find.byType(Radio<int>))), |
1038 | paints |
1039 | ..rect() |
1040 | ..circle() |
1041 | ..circle(color: hoveredFillColor), |
1042 | ); |
1043 | }); |
1044 | |
1045 | testWidgets('Material3 - RadioListTile respects hoverColor' , (WidgetTester tester) async { |
1046 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
1047 | int? groupValue = 0; |
1048 | final Color? hoverColor = Colors.orange[500]; |
1049 | final ThemeData theme = ThemeData(); |
1050 | Widget buildApp({bool enabled = true}) { |
1051 | return wrap( |
1052 | child: MaterialApp( |
1053 | theme: theme, |
1054 | home: StatefulBuilder( |
1055 | builder: (BuildContext context, StateSetter setState) { |
1056 | return RadioListTile<int>( |
1057 | value: 0, |
1058 | onChanged: |
1059 | enabled |
1060 | ? (int? newValue) { |
1061 | setState(() { |
1062 | groupValue = newValue; |
1063 | }); |
1064 | } |
1065 | : null, |
1066 | hoverColor: hoverColor, |
1067 | groupValue: groupValue, |
1068 | ); |
1069 | }, |
1070 | ), |
1071 | ), |
1072 | ); |
1073 | } |
1074 | |
1075 | await tester.pumpWidget(buildApp()); |
1076 | |
1077 | await tester.pump(); |
1078 | await tester.pumpAndSettle(); |
1079 | expect( |
1080 | Material.of(tester.element(find.byType(Radio<int>))), |
1081 | paints |
1082 | ..rect() |
1083 | ..circle(color: theme.colorScheme.primary) |
1084 | ..circle(color: theme.colorScheme.primary), |
1085 | ); |
1086 | |
1087 | // Start hovering |
1088 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1089 | await gesture.moveTo(tester.getCenter(find.byType(Radio<int>))); |
1090 | |
1091 | // Check when the radio isn't selected. |
1092 | groupValue = 1; |
1093 | await tester.pumpWidget(buildApp()); |
1094 | await tester.pump(); |
1095 | await tester.pumpAndSettle(); |
1096 | expect( |
1097 | Material.of(tester.element(find.byType(Radio<int>))), |
1098 | paints |
1099 | ..rect() |
1100 | ..circle(color: hoverColor), |
1101 | ); |
1102 | |
1103 | // Check when the radio is selected, but disabled. |
1104 | groupValue = 0; |
1105 | await tester.pumpWidget(buildApp(enabled: false)); |
1106 | await tester.pump(); |
1107 | await tester.pumpAndSettle(); |
1108 | expect( |
1109 | Material.of(tester.element(find.byType(Radio<int>))), |
1110 | paints |
1111 | ..rect() |
1112 | ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)) |
1113 | ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)), |
1114 | ); |
1115 | }); |
1116 | |
1117 | testWidgets('Material3 - RadioListTile respects overlayColor in active/pressed/hovered states' , ( |
1118 | WidgetTester tester, |
1119 | ) async { |
1120 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
1121 | |
1122 | const Color fillColor = Color(0xFF000000); |
1123 | const Color activePressedOverlayColor = Color(0xFF000001); |
1124 | const Color inactivePressedOverlayColor = Color(0xFF000002); |
1125 | const Color hoverOverlayColor = Color(0xFF000003); |
1126 | const Color hoverColor = Color(0xFF000005); |
1127 | |
1128 | Color? getOverlayColor(Set<MaterialState> states) { |
1129 | if (states.contains(MaterialState.pressed)) { |
1130 | if (states.contains(MaterialState.selected)) { |
1131 | return activePressedOverlayColor; |
1132 | } |
1133 | return inactivePressedOverlayColor; |
1134 | } |
1135 | if (states.contains(MaterialState.hovered)) { |
1136 | return hoverOverlayColor; |
1137 | } |
1138 | return null; |
1139 | } |
1140 | |
1141 | Widget buildRadio({bool active = false, bool useOverlay = true}) { |
1142 | return MaterialApp( |
1143 | home: Material( |
1144 | child: RadioListTile<bool>( |
1145 | value: active, |
1146 | groupValue: true, |
1147 | onChanged: (_) {}, |
1148 | fillColor: const MaterialStatePropertyAll<Color>(fillColor), |
1149 | overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null, |
1150 | hoverColor: hoverColor, |
1151 | ), |
1152 | ), |
1153 | ); |
1154 | } |
1155 | |
1156 | await tester.pumpWidget(buildRadio(useOverlay: false)); |
1157 | await tester.press(find.byType(Radio<bool>)); |
1158 | await tester.pumpAndSettle(); |
1159 | |
1160 | expect( |
1161 | Material.of(tester.element(find.byType(Radio<bool>))), |
1162 | paints |
1163 | ..rect(color: const Color(0x00000000)) |
1164 | ..rect(color: const Color(0x66bcbcbc)) |
1165 | ..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: 20.0), |
1166 | reason: 'Default inactive pressed Radio should have overlay color from fillColor' , |
1167 | ); |
1168 | |
1169 | await tester.pumpWidget(buildRadio(active: true, useOverlay: false)); |
1170 | await tester.press(find.byType(Radio<bool>)); |
1171 | await tester.pumpAndSettle(); |
1172 | |
1173 | expect( |
1174 | Material.of(tester.element(find.byType(Radio<bool>))), |
1175 | paints |
1176 | ..rect(color: const Color(0x00000000)) |
1177 | ..rect(color: const Color(0x66bcbcbc)) |
1178 | ..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: 20.0), |
1179 | reason: 'Default active pressed Radio should have overlay color from fillColor' , |
1180 | ); |
1181 | |
1182 | await tester.pumpWidget(buildRadio()); |
1183 | await tester.press(find.byType(Radio<bool>)); |
1184 | await tester.pumpAndSettle(); |
1185 | |
1186 | expect( |
1187 | Material.of(tester.element(find.byType(Radio<bool>))), |
1188 | paints |
1189 | ..rect(color: const Color(0x00000000)) |
1190 | ..rect(color: const Color(0x66bcbcbc)) |
1191 | ..circle(color: inactivePressedOverlayColor, radius: 20.0), |
1192 | reason: 'Inactive pressed Radio should have overlay color: $inactivePressedOverlayColor' , |
1193 | ); |
1194 | |
1195 | await tester.pumpWidget(buildRadio(active: true)); |
1196 | await tester.press(find.byType(Radio<bool>)); |
1197 | await tester.pumpAndSettle(); |
1198 | |
1199 | expect( |
1200 | Material.of(tester.element(find.byType(Radio<bool>))), |
1201 | paints |
1202 | ..rect(color: const Color(0x00000000)) |
1203 | ..rect(color: const Color(0x66bcbcbc)) |
1204 | ..circle(color: activePressedOverlayColor, radius: 20.0), |
1205 | reason: 'Active pressed Radio should have overlay color: $activePressedOverlayColor' , |
1206 | ); |
1207 | |
1208 | // Start hovering. |
1209 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1210 | await gesture.addPointer(); |
1211 | await gesture.moveTo(tester.getCenter(find.byType(Radio<bool>))); |
1212 | await tester.pumpAndSettle(); |
1213 | |
1214 | await tester.pumpWidget(Container()); |
1215 | await tester.pumpWidget(buildRadio()); |
1216 | await tester.pumpAndSettle(); |
1217 | |
1218 | expect( |
1219 | Material.of(tester.element(find.byType(Radio<bool>))), |
1220 | paints |
1221 | ..rect(color: const Color(0x00000000)) |
1222 | ..rect(color: const Color(0x0a000000)) |
1223 | ..circle(color: hoverOverlayColor, radius: 20.0), |
1224 | reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor' , |
1225 | ); |
1226 | }); |
1227 | |
1228 | testWidgets('RadioListTile respects splashRadius' , (WidgetTester tester) async { |
1229 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
1230 | const double splashRadius = 30; |
1231 | Widget buildApp() { |
1232 | return wrap( |
1233 | child: StatefulBuilder( |
1234 | builder: (BuildContext context, StateSetter setState) { |
1235 | return RadioListTile<int>( |
1236 | value: 0, |
1237 | onChanged: (_) {}, |
1238 | hoverColor: Colors.orange[500], |
1239 | groupValue: 0, |
1240 | splashRadius: splashRadius, |
1241 | ); |
1242 | }, |
1243 | ), |
1244 | ); |
1245 | } |
1246 | |
1247 | await tester.pumpWidget(buildApp()); |
1248 | await tester.pumpAndSettle(); |
1249 | |
1250 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1251 | await gesture.addPointer(); |
1252 | await gesture.moveTo(tester.getCenter(find.byType(Radio<int>))); |
1253 | await tester.pumpAndSettle(); |
1254 | |
1255 | expect( |
1256 | Material.of(tester.element(find.byWidgetPredicate((Widget widget) => widget is Radio<int>))), |
1257 | paints..circle(color: Colors.orange[500], radius: splashRadius), |
1258 | ); |
1259 | }); |
1260 | |
1261 | testWidgets('Radio respects materialTapTargetSize' , (WidgetTester tester) async { |
1262 | await tester.pumpWidget( |
1263 | wrap( |
1264 | child: RadioListTile<bool>(groupValue: true, value: true, onChanged: (bool? newValue) {}), |
1265 | ), |
1266 | ); |
1267 | |
1268 | // default test |
1269 | expect(tester.getSize(find.byType(Radio<bool>)), const Size(40.0, 40.0)); |
1270 | |
1271 | await tester.pumpWidget( |
1272 | wrap( |
1273 | child: RadioListTile<bool>( |
1274 | materialTapTargetSize: MaterialTapTargetSize.padded, |
1275 | groupValue: true, |
1276 | value: true, |
1277 | onChanged: (bool? newValue) {}, |
1278 | ), |
1279 | ), |
1280 | ); |
1281 | |
1282 | expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0)); |
1283 | }); |
1284 | |
1285 | testWidgets('RadioListTile.control widget should not request focus on traversal' , ( |
1286 | WidgetTester tester, |
1287 | ) async { |
1288 | final GlobalKey firstChildKey = GlobalKey(); |
1289 | final GlobalKey secondChildKey = GlobalKey(); |
1290 | |
1291 | await tester.pumpWidget( |
1292 | MaterialApp( |
1293 | home: Material( |
1294 | child: Column( |
1295 | children: <Widget>[ |
1296 | RadioListTile<bool>( |
1297 | value: true, |
1298 | groupValue: true, |
1299 | onChanged: (bool? value) {}, |
1300 | title: Text('Hey' , key: firstChildKey), |
1301 | ), |
1302 | RadioListTile<bool>( |
1303 | value: true, |
1304 | groupValue: true, |
1305 | onChanged: (bool? value) {}, |
1306 | title: Text('There' , key: secondChildKey), |
1307 | ), |
1308 | ], |
1309 | ), |
1310 | ), |
1311 | ), |
1312 | ); |
1313 | |
1314 | await tester.pump(); |
1315 | Focus.of(firstChildKey.currentContext!).requestFocus(); |
1316 | await tester.pump(); |
1317 | expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue); |
1318 | Focus.of(firstChildKey.currentContext!).nextFocus(); |
1319 | await tester.pump(); |
1320 | expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse); |
1321 | expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue); |
1322 | }); |
1323 | |
1324 | testWidgets('RadioListTile.adaptive shows the correct radio platform widget' , ( |
1325 | WidgetTester tester, |
1326 | ) async { |
1327 | Widget buildApp(TargetPlatform platform) { |
1328 | return MaterialApp( |
1329 | theme: ThemeData(platform: platform), |
1330 | home: Material( |
1331 | child: Center( |
1332 | child: RadioListTile<int>.adaptive(value: 1, groupValue: 2, onChanged: (_) {}), |
1333 | ), |
1334 | ), |
1335 | ); |
1336 | } |
1337 | |
1338 | for (final TargetPlatform platform in <TargetPlatform>[ |
1339 | TargetPlatform.iOS, |
1340 | TargetPlatform.macOS, |
1341 | ]) { |
1342 | await tester.pumpWidget(buildApp(platform)); |
1343 | await tester.pumpAndSettle(); |
1344 | |
1345 | expect(find.byType(CupertinoRadio<int>), findsOneWidget); |
1346 | } |
1347 | |
1348 | for (final TargetPlatform platform in <TargetPlatform>[ |
1349 | TargetPlatform.android, |
1350 | TargetPlatform.fuchsia, |
1351 | TargetPlatform.linux, |
1352 | TargetPlatform.windows, |
1353 | ]) { |
1354 | await tester.pumpWidget(buildApp(platform)); |
1355 | await tester.pumpAndSettle(); |
1356 | |
1357 | expect(find.byType(CupertinoRadio<int>), findsNothing); |
1358 | } |
1359 | }); |
1360 | |
1361 | group('feedback' , () { |
1362 | late FeedbackTester feedback; |
1363 | |
1364 | setUp(() { |
1365 | feedback = FeedbackTester(); |
1366 | }); |
1367 | |
1368 | tearDown(() { |
1369 | feedback.dispose(); |
1370 | }); |
1371 | |
1372 | testWidgets('RadioListTile respects enableFeedback' , (WidgetTester tester) async { |
1373 | const Key key = Key('test' ); |
1374 | Future<void> buildTest(bool enableFeedback) async { |
1375 | return tester.pumpWidget( |
1376 | wrap( |
1377 | child: Center( |
1378 | child: RadioListTile<bool>( |
1379 | key: key, |
1380 | value: false, |
1381 | groupValue: true, |
1382 | selected: true, |
1383 | onChanged: (bool? value) {}, |
1384 | enableFeedback: enableFeedback, |
1385 | ), |
1386 | ), |
1387 | ), |
1388 | ); |
1389 | } |
1390 | |
1391 | await buildTest(false); |
1392 | await tester.tap(find.byKey(key)); |
1393 | await tester.pump(const Duration(seconds: 1)); |
1394 | expect(feedback.clickSoundCount, 0); |
1395 | expect(feedback.hapticCount, 0); |
1396 | |
1397 | await buildTest(true); |
1398 | await tester.tap(find.byKey(key)); |
1399 | await tester.pump(const Duration(seconds: 1)); |
1400 | expect(feedback.clickSoundCount, 1); |
1401 | expect(feedback.hapticCount, 0); |
1402 | }); |
1403 | }); |
1404 | |
1405 | group('Material 2' , () { |
1406 | // These tests are only relevant for Material 2. Once Material 2 |
1407 | // support is deprecated and the APIs are removed, these tests |
1408 | // can be deleted. |
1409 | |
1410 | testWidgets( |
1411 | 'Material2 - RadioListTile respects overlayColor in active/pressed/hovered states' , |
1412 | (WidgetTester tester) async { |
1413 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
1414 | |
1415 | const Color fillColor = Color(0xFF000000); |
1416 | const Color activePressedOverlayColor = Color(0xFF000001); |
1417 | const Color inactivePressedOverlayColor = Color(0xFF000002); |
1418 | const Color hoverOverlayColor = Color(0xFF000003); |
1419 | const Color hoverColor = Color(0xFF000005); |
1420 | |
1421 | Color? getOverlayColor(Set<MaterialState> states) { |
1422 | if (states.contains(MaterialState.pressed)) { |
1423 | if (states.contains(MaterialState.selected)) { |
1424 | return activePressedOverlayColor; |
1425 | } |
1426 | return inactivePressedOverlayColor; |
1427 | } |
1428 | if (states.contains(MaterialState.hovered)) { |
1429 | return hoverOverlayColor; |
1430 | } |
1431 | return null; |
1432 | } |
1433 | |
1434 | Widget buildRadio({bool active = false, bool useOverlay = true}) { |
1435 | return MaterialApp( |
1436 | theme: ThemeData(useMaterial3: false), |
1437 | home: Material( |
1438 | child: RadioListTile<bool>( |
1439 | value: active, |
1440 | groupValue: true, |
1441 | onChanged: (_) {}, |
1442 | fillColor: const MaterialStatePropertyAll<Color>(fillColor), |
1443 | overlayColor: |
1444 | useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null, |
1445 | hoverColor: hoverColor, |
1446 | ), |
1447 | ), |
1448 | ); |
1449 | } |
1450 | |
1451 | await tester.pumpWidget(buildRadio(useOverlay: false)); |
1452 | await tester.press(find.byType(Radio<bool>)); |
1453 | await tester.pumpAndSettle(); |
1454 | |
1455 | expect( |
1456 | Material.of(tester.element(find.byType(Radio<bool>))), |
1457 | paints |
1458 | ..circle() |
1459 | ..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: 20), |
1460 | reason: 'Default inactive pressed Radio should have overlay color from fillColor' , |
1461 | ); |
1462 | |
1463 | await tester.pumpWidget(buildRadio(active: true, useOverlay: false)); |
1464 | await tester.press(find.byType(Radio<bool>)); |
1465 | await tester.pumpAndSettle(); |
1466 | |
1467 | expect( |
1468 | Material.of(tester.element(find.byType(Radio<bool>))), |
1469 | paints |
1470 | ..circle() |
1471 | ..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: 20), |
1472 | reason: 'Default active pressed Radio should have overlay color from fillColor' , |
1473 | ); |
1474 | |
1475 | await tester.pumpWidget(buildRadio()); |
1476 | await tester.press(find.byType(Radio<bool>)); |
1477 | await tester.pumpAndSettle(); |
1478 | |
1479 | expect( |
1480 | Material.of(tester.element(find.byType(Radio<bool>))), |
1481 | paints |
1482 | ..circle() |
1483 | ..circle(color: inactivePressedOverlayColor, radius: 20), |
1484 | reason: 'Inactive pressed Radio should have overlay color: $inactivePressedOverlayColor' , |
1485 | ); |
1486 | |
1487 | // Start hovering. |
1488 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1489 | await gesture.addPointer(); |
1490 | await gesture.moveTo(tester.getCenter(find.byType(Radio<bool>))); |
1491 | await tester.pumpAndSettle(); |
1492 | |
1493 | await tester.pumpWidget(Container()); |
1494 | await tester.pumpWidget(buildRadio()); |
1495 | await tester.pumpAndSettle(); |
1496 | |
1497 | expect( |
1498 | Material.of(tester.element(find.byType(Radio<bool>))), |
1499 | paints..circle(color: hoverOverlayColor, radius: 20), |
1500 | reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor' , |
1501 | ); |
1502 | }, |
1503 | ); |
1504 | |
1505 | testWidgets('Material2 - RadioListTile respects hoverColor' , (WidgetTester tester) async { |
1506 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
1507 | int? groupValue = 0; |
1508 | final Color? hoverColor = Colors.orange[500]; |
1509 | Widget buildApp({bool enabled = true}) { |
1510 | return wrap( |
1511 | child: MaterialApp( |
1512 | theme: ThemeData(useMaterial3: false), |
1513 | home: StatefulBuilder( |
1514 | builder: (BuildContext context, StateSetter setState) { |
1515 | return RadioListTile<int>( |
1516 | value: 0, |
1517 | onChanged: |
1518 | enabled |
1519 | ? (int? newValue) { |
1520 | setState(() { |
1521 | groupValue = newValue; |
1522 | }); |
1523 | } |
1524 | : null, |
1525 | hoverColor: hoverColor, |
1526 | groupValue: groupValue, |
1527 | ); |
1528 | }, |
1529 | ), |
1530 | ), |
1531 | ); |
1532 | } |
1533 | |
1534 | await tester.pumpWidget(buildApp()); |
1535 | |
1536 | await tester.pump(); |
1537 | await tester.pumpAndSettle(); |
1538 | expect( |
1539 | Material.of(tester.element(find.byType(Radio<int>))), |
1540 | paints |
1541 | ..rect() |
1542 | ..circle(color: const Color(0xff2196f3)) |
1543 | ..circle(color: const Color(0xff2196f3)), |
1544 | ); |
1545 | |
1546 | // Start hovering |
1547 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1548 | await gesture.moveTo(tester.getCenter(find.byType(Radio<int>))); |
1549 | |
1550 | // Check when the radio isn't selected. |
1551 | groupValue = 1; |
1552 | await tester.pumpWidget(buildApp()); |
1553 | await tester.pump(); |
1554 | await tester.pumpAndSettle(); |
1555 | expect( |
1556 | Material.of(tester.element(find.byType(Radio<int>))), |
1557 | paints |
1558 | ..rect() |
1559 | ..circle(color: hoverColor), |
1560 | ); |
1561 | |
1562 | // Check when the radio is selected, but disabled. |
1563 | groupValue = 0; |
1564 | await tester.pumpWidget(buildApp(enabled: false)); |
1565 | await tester.pump(); |
1566 | await tester.pumpAndSettle(); |
1567 | expect( |
1568 | Material.of(tester.element(find.byType(Radio<int>))), |
1569 | paints |
1570 | ..rect() |
1571 | ..circle(color: const Color(0x61000000)) |
1572 | ..circle(color: const Color(0x61000000)), |
1573 | ); |
1574 | }); |
1575 | }); |
1576 | |
1577 | testWidgets('RadioListTile uses ListTileTheme controlAffinity' , (WidgetTester tester) async { |
1578 | Widget buildListTile(ListTileControlAffinity controlAffinity) { |
1579 | return MaterialApp( |
1580 | home: Material( |
1581 | child: ListTileTheme( |
1582 | data: ListTileThemeData(controlAffinity: controlAffinity), |
1583 | child: RadioListTile<double>( |
1584 | value: 0.5, |
1585 | groupValue: 1.0, |
1586 | title: const Text('RadioListTile' ), |
1587 | onChanged: (double? value) {}, |
1588 | ), |
1589 | ), |
1590 | ), |
1591 | ); |
1592 | } |
1593 | |
1594 | await tester.pumpWidget(buildListTile(ListTileControlAffinity.leading)); |
1595 | final Finder leading = find.text('RadioListTile' ); |
1596 | final Offset offsetLeading = tester.getTopLeft(leading); |
1597 | expect(offsetLeading, const Offset(72.0, 16.0)); |
1598 | |
1599 | await tester.pumpWidget(buildListTile(ListTileControlAffinity.trailing)); |
1600 | final Finder trailing = find.text('RadioListTile' ); |
1601 | final Offset offsetTrailing = tester.getTopLeft(trailing); |
1602 | expect(offsetTrailing, const Offset(16.0, 16.0)); |
1603 | |
1604 | await tester.pumpWidget(buildListTile(ListTileControlAffinity.platform)); |
1605 | final Finder platform = find.text('RadioListTile' ); |
1606 | final Offset offsetPlatform = tester.getTopLeft(platform); |
1607 | expect(offsetPlatform, const Offset(72.0, 16.0)); |
1608 | }); |
1609 | |
1610 | testWidgets('RadioListTile renders with default scale' , (WidgetTester tester) async { |
1611 | await tester.pumpWidget( |
1612 | const MaterialApp( |
1613 | home: Material( |
1614 | child: RadioListTile<bool>(value: false, groupValue: false, onChanged: null), |
1615 | ), |
1616 | ), |
1617 | ); |
1618 | |
1619 | final Finder transformFinder = find.ancestor( |
1620 | of: find.byType(Radio<bool>), |
1621 | matching: find.byType(Transform), |
1622 | ); |
1623 | |
1624 | expect(transformFinder, findsNothing); |
1625 | }); |
1626 | |
1627 | testWidgets('RadioListTile respects radioScaleFactor' , (WidgetTester tester) async { |
1628 | const double scale = 1.4; |
1629 | await tester.pumpWidget( |
1630 | const MaterialApp( |
1631 | home: Material( |
1632 | child: RadioListTile<bool>( |
1633 | value: false, |
1634 | groupValue: false, |
1635 | onChanged: null, |
1636 | radioScaleFactor: scale, |
1637 | ), |
1638 | ), |
1639 | ), |
1640 | ); |
1641 | |
1642 | final Transform widget = tester.widget( |
1643 | find.ancestor(of: find.byType(Radio<bool>), matching: find.byType(Transform)), |
1644 | ); |
1645 | |
1646 | expect(widget.transform.getMaxScaleOnAxis(), scale); |
1647 | }); |
1648 | } |
1649 | |