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// This file is run as part of a reduced test set in CI on Mac and Windows
6// machines.
7@Tags(<String>['reduced-test-set'])
8library;
9
10import 'dart:ui';
11
12import 'package:flutter/material.dart';
13import 'package:flutter/rendering.dart';
14import 'package:flutter/services.dart';
15import 'package:flutter_test/flutter_test.dart';
16
17import '../widgets/semantics_tester.dart';
18
19void main() {
20 RenderObject getOverlayColor(WidgetTester tester) {
21 return tester.allRenderObjects.firstWhere(
22 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
23 );
24 }
25
26 Widget boilerplate({required Widget child}) {
27 return Directionality(
28 textDirection: TextDirection.ltr,
29 child: Center(child: child),
30 );
31 }
32
33 TextStyle iconStyle(WidgetTester tester, IconData icon) {
34 final RichText iconRichText = tester.widget<RichText>(
35 find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
36 );
37 return iconRichText.text.style!;
38 }
39
40 testWidgets('SegmentsButton when compositing does not crash', (WidgetTester tester) async {
41 // Regression test for https://github.com/flutter/flutter/issues/135747
42 // If the render object holds on to a stale canvas reference, this will
43 // throw an exception.
44 await tester.pumpWidget(
45 MaterialApp(
46 home: Scaffold(
47 body: SegmentedButton<int>(
48 segments: const <ButtonSegment<int>>[
49 ButtonSegment<int>(
50 value: 0,
51 label: Opacity(opacity: 0.5, child: Text('option')),
52 icon: Opacity(opacity: 0.5, child: Icon(Icons.add)),
53 ),
54 ],
55 selected: const <int>{0},
56 ),
57 ),
58 ),
59 );
60
61 expect(find.byType(SegmentedButton<int>), findsOneWidget);
62 expect(tester.takeException(), isNull);
63 });
64
65 testWidgets('SegmentedButton releases state controllers for deleted segments', (
66 WidgetTester tester,
67 ) async {
68 final ThemeData theme = ThemeData();
69 final Key key = UniqueKey();
70
71 Widget buildApp(Widget button) {
72 return MaterialApp(
73 theme: theme,
74 home: Scaffold(body: Center(child: button)),
75 );
76 }
77
78 await tester.pumpWidget(
79 buildApp(
80 SegmentedButton<int>(
81 key: key,
82 segments: const <ButtonSegment<int>>[
83 ButtonSegment<int>(value: 1, label: Text('1')),
84 ButtonSegment<int>(value: 2, label: Text('2')),
85 ],
86 selected: const <int>{2},
87 ),
88 ),
89 );
90
91 await tester.pumpWidget(
92 buildApp(
93 SegmentedButton<int>(
94 key: key,
95 segments: const <ButtonSegment<int>>[
96 ButtonSegment<int>(value: 2, label: Text('2')),
97 ButtonSegment<int>(value: 3, label: Text('3')),
98 ],
99 selected: const <int>{2},
100 ),
101 ),
102 );
103
104 final SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>));
105 expect(state.statesControllers, hasLength(2));
106 expect(state.statesControllers.keys.first.value, 2);
107 expect(state.statesControllers.keys.last.value, 3);
108 });
109
110 testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (
111 WidgetTester tester,
112 ) async {
113 final ThemeData theme = ThemeData();
114 await tester.pumpWidget(
115 MaterialApp(
116 theme: theme,
117 home: Scaffold(
118 body: Center(
119 child: SegmentedButton<int>(
120 segments: const <ButtonSegment<int>>[
121 ButtonSegment<int>(value: 1, label: Text('1')),
122 ButtonSegment<int>(value: 2, label: Text('2')),
123 ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
124 ],
125 selected: const <int>{2},
126 onSelectionChanged: (Set<int> selected) {},
127 ),
128 ),
129 ),
130 ),
131 );
132
133 // Expect SegmentedButton to be built with type MaterialType.transparency.
134 final Finder text = find.text('1');
135 final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
136 final Finder parentMaterial = find.ancestor(of: parent, matching: find.byType(Material)).first;
137 final Material material = tester.widget<Material>(parentMaterial);
138 expect(material.type, MaterialType.transparency);
139 });
140
141 testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async {
142 int callbackCount = 0;
143 int selectedSegment = 2;
144
145 Widget frameWithSelection(int selected) {
146 return Material(
147 child: boilerplate(
148 child: SegmentedButton<int>(
149 segments: const <ButtonSegment<int>>[
150 ButtonSegment<int>(value: 1, label: Text('1')),
151 ButtonSegment<int>(value: 2, label: Text('2')),
152 ButtonSegment<int>(value: 3, label: Text('3')),
153 ],
154 selected: <int>{selected},
155 onSelectionChanged: (Set<int> selected) {
156 assert(selected.length == 1);
157 selectedSegment = selected.first;
158 callbackCount += 1;
159 },
160 ),
161 ),
162 );
163 }
164
165 await tester.pumpWidget(frameWithSelection(selectedSegment));
166 expect(selectedSegment, 2);
167 expect(callbackCount, 0);
168
169 // Tap on segment 1.
170 await tester.tap(find.text('1'));
171 await tester.pumpAndSettle();
172 expect(callbackCount, 1);
173 expect(selectedSegment, 1);
174
175 // Update the selection in the widget
176 await tester.pumpWidget(frameWithSelection(1));
177
178 // Tap on segment 1 again should do nothing.
179 await tester.tap(find.text('1'));
180 await tester.pumpAndSettle();
181 expect(callbackCount, 1);
182 expect(selectedSegment, 1);
183
184 // Tap on segment 3.
185 await tester.tap(find.text('3'));
186 await tester.pumpAndSettle();
187 expect(callbackCount, 2);
188 expect(selectedSegment, 3);
189 });
190
191 testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async {
192 int callbackCount = 0;
193 Set<int> selection = <int>{1};
194
195 Widget frameWithSelection(Set<int> selected) {
196 return Material(
197 child: boilerplate(
198 child: SegmentedButton<int>(
199 multiSelectionEnabled: true,
200 segments: const <ButtonSegment<int>>[
201 ButtonSegment<int>(value: 1, label: Text('1')),
202 ButtonSegment<int>(value: 2, label: Text('2')),
203 ButtonSegment<int>(value: 3, label: Text('3')),
204 ],
205 selected: selected,
206 onSelectionChanged: (Set<int> selected) {
207 selection = selected;
208 callbackCount += 1;
209 },
210 ),
211 ),
212 );
213 }
214
215 await tester.pumpWidget(frameWithSelection(selection));
216 expect(selection, <int>{1});
217 expect(callbackCount, 0);
218
219 // Tap on segment 2.
220 await tester.tap(find.text('2'));
221 await tester.pumpAndSettle();
222 expect(callbackCount, 1);
223 expect(selection, <int>{1, 2});
224
225 // Update the selection in the widget
226 await tester.pumpWidget(frameWithSelection(<int>{1, 2}));
227 await tester.pumpAndSettle();
228
229 // Tap on segment 1 again should remove it from selection.
230 await tester.tap(find.text('1'));
231 await tester.pumpAndSettle();
232 expect(callbackCount, 2);
233 expect(selection, <int>{2});
234
235 // Update the selection in the widget
236 await tester.pumpWidget(frameWithSelection(<int>{2}));
237 await tester.pumpAndSettle();
238
239 // Tap on segment 3.
240 await tester.tap(find.text('3'));
241 await tester.pumpAndSettle();
242 expect(callbackCount, 3);
243 expect(selection, <int>{2, 3});
244 });
245
246 testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async {
247 int callbackCount = 0;
248 int? selectedSegment = 1;
249
250 Widget frameWithSelection(int? selected) {
251 return Material(
252 child: boilerplate(
253 child: SegmentedButton<int>(
254 emptySelectionAllowed: true,
255 segments: const <ButtonSegment<int>>[
256 ButtonSegment<int>(value: 1, label: Text('1')),
257 ButtonSegment<int>(value: 2, label: Text('2')),
258 ButtonSegment<int>(value: 3, label: Text('3')),
259 ],
260 selected: <int>{if (selected != null) selected},
261 onSelectionChanged: (Set<int> selected) {
262 selectedSegment = selected.isEmpty ? null : selected.first;
263 callbackCount += 1;
264 },
265 ),
266 ),
267 );
268 }
269
270 await tester.pumpWidget(frameWithSelection(selectedSegment));
271 expect(selectedSegment, 1);
272 expect(callbackCount, 0);
273
274 // Tap on segment 1 should deselect it and make the selection empty.
275 await tester.tap(find.text('1'));
276 await tester.pumpAndSettle();
277 expect(callbackCount, 1);
278 expect(selectedSegment, null);
279
280 // Update the selection in the widget
281 await tester.pumpWidget(frameWithSelection(null));
282
283 // Tap on segment 2 should select it.
284 await tester.tap(find.text('2'));
285 await tester.pumpAndSettle();
286 expect(callbackCount, 2);
287 expect(selectedSegment, 2);
288
289 // Update the selection in the widget
290 await tester.pumpWidget(frameWithSelection(2));
291
292 // Tap on segment 3.
293 await tester.tap(find.text('3'));
294 await tester.pumpAndSettle();
295 expect(callbackCount, 3);
296 expect(selectedSegment, 3);
297 });
298
299 testWidgets('SegmentedButton shows checkboxes for selected segments', (
300 WidgetTester tester,
301 ) async {
302 Widget frameWithSelection(int selected) {
303 return Material(
304 child: boilerplate(
305 child: SegmentedButton<int>(
306 segments: const <ButtonSegment<int>>[
307 ButtonSegment<int>(value: 1, label: Text('1')),
308 ButtonSegment<int>(value: 2, label: Text('2')),
309 ButtonSegment<int>(value: 3, label: Text('3')),
310 ],
311 selected: <int>{selected},
312 onSelectionChanged: (Set<int> selected) {},
313 ),
314 ),
315 );
316 }
317
318 Finder textHasIcon(String text, IconData icon) {
319 return find.descendant(of: find.widgetWithText(Row, text), matching: find.byIcon(icon));
320 }
321
322 await tester.pumpWidget(frameWithSelection(1));
323 expect(textHasIcon('1', Icons.check), findsOneWidget);
324 expect(find.byIcon(Icons.check), findsOneWidget);
325
326 await tester.pumpWidget(frameWithSelection(2));
327 expect(textHasIcon('2', Icons.check), findsOneWidget);
328 expect(find.byIcon(Icons.check), findsOneWidget);
329
330 await tester.pumpWidget(frameWithSelection(2));
331 expect(textHasIcon('2', Icons.check), findsOneWidget);
332 expect(find.byIcon(Icons.check), findsOneWidget);
333 });
334
335 testWidgets(
336 'SegmentedButton shows selected checkboxes in place of icon if it has a label as well',
337 (WidgetTester tester) async {
338 Widget frameWithSelection(int selected) {
339 return Material(
340 child: boilerplate(
341 child: SegmentedButton<int>(
342 segments: const <ButtonSegment<int>>[
343 ButtonSegment<int>(value: 1, icon: Icon(Icons.add), label: Text('1')),
344 ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')),
345 ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')),
346 ],
347 selected: <int>{selected},
348 onSelectionChanged: (Set<int> selected) {},
349 ),
350 ),
351 );
352 }
353
354 Finder textHasIcon(String text, IconData icon) {
355 return find.descendant(of: find.widgetWithText(Row, text), matching: find.byIcon(icon));
356 }
357
358 await tester.pumpWidget(frameWithSelection(1));
359 expect(textHasIcon('1', Icons.check), findsOneWidget);
360 expect(find.byIcon(Icons.add), findsNothing);
361 expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
362 expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
363
364 await tester.pumpWidget(frameWithSelection(2));
365 expect(textHasIcon('1', Icons.add), findsOneWidget);
366 expect(textHasIcon('2', Icons.check), findsOneWidget);
367 expect(find.byIcon(Icons.add_a_photo), findsNothing);
368 expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
369
370 await tester.pumpWidget(frameWithSelection(3));
371 expect(textHasIcon('1', Icons.add), findsOneWidget);
372 expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
373 expect(textHasIcon('3', Icons.check), findsOneWidget);
374 expect(find.byIcon(Icons.add_alarm), findsNothing);
375 },
376 );
377
378 testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', (
379 WidgetTester tester,
380 ) async {
381 Widget frameWithSelection(int selected) {
382 return Material(
383 child: boilerplate(
384 child: SegmentedButton<int>(
385 segments: const <ButtonSegment<int>>[
386 ButtonSegment<int>(value: 1, icon: Icon(Icons.add)),
387 ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo)),
388 ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm)),
389 ],
390 selected: <int>{selected},
391 onSelectionChanged: (Set<int> selected) {},
392 ),
393 ),
394 );
395 }
396
397 Finder rowWithIcons(IconData icon1, IconData icon2) {
398 return find.descendant(of: find.widgetWithIcon(Row, icon1), matching: find.byIcon(icon2));
399 }
400
401 await tester.pumpWidget(frameWithSelection(1));
402 expect(rowWithIcons(Icons.add, Icons.check), findsOneWidget);
403 expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
404 expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
405
406 await tester.pumpWidget(frameWithSelection(2));
407 expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
408 expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsOneWidget);
409 expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
410
411 await tester.pumpWidget(frameWithSelection(3));
412 expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
413 expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
414 expect(rowWithIcons(Icons.add_alarm, Icons.check), findsOneWidget);
415 });
416
417 testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async {
418 final SemanticsTester semantics = SemanticsTester(tester);
419
420 await tester.pumpWidget(
421 Material(
422 child: boilerplate(
423 child: SegmentedButton<int>(
424 segments: const <ButtonSegment<int>>[
425 ButtonSegment<int>(value: 1, label: Text('1')),
426 ButtonSegment<int>(value: 2, label: Text('2')),
427 ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
428 ],
429 selected: const <int>{2},
430 onSelectionChanged: (Set<int> selected) {},
431 ),
432 ),
433 ),
434 );
435
436 expect(
437 semantics,
438 hasSemantics(
439 TestSemantics.root(
440 children: <TestSemantics>[
441 // First is an unselected, enabled button.
442 TestSemantics(
443 flags: <SemanticsFlag>[
444 SemanticsFlag.isButton,
445 SemanticsFlag.isEnabled,
446 SemanticsFlag.hasEnabledState,
447 SemanticsFlag.hasCheckedState,
448 SemanticsFlag.isFocusable,
449 SemanticsFlag.isInMutuallyExclusiveGroup,
450 ],
451 label: '1',
452 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
453 ),
454
455 // Second is a selected, enabled button.
456 TestSemantics(
457 flags: <SemanticsFlag>[
458 SemanticsFlag.isButton,
459 SemanticsFlag.isEnabled,
460 SemanticsFlag.hasEnabledState,
461 SemanticsFlag.hasCheckedState,
462 SemanticsFlag.isChecked,
463 SemanticsFlag.isFocusable,
464 SemanticsFlag.isInMutuallyExclusiveGroup,
465 ],
466 label: '2',
467 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
468 ),
469
470 // Third is an unselected, disabled button.
471 TestSemantics(
472 flags: <SemanticsFlag>[
473 SemanticsFlag.isButton,
474 SemanticsFlag.hasEnabledState,
475 SemanticsFlag.hasCheckedState,
476 SemanticsFlag.isInMutuallyExclusiveGroup,
477 ],
478 label: '3',
479 ),
480 ],
481 ),
482 ignoreId: true,
483 ignoreRect: true,
484 ignoreTransform: true,
485 ),
486 );
487
488 semantics.dispose();
489 });
490
491 testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async {
492 final SemanticsTester semantics = SemanticsTester(tester);
493
494 await tester.pumpWidget(
495 Material(
496 child: boilerplate(
497 child: SegmentedButton<int>(
498 segments: const <ButtonSegment<int>>[
499 ButtonSegment<int>(value: 1, label: Text('1')),
500 ButtonSegment<int>(value: 2, label: Text('2')),
501 ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
502 ],
503 selected: const <int>{1, 3},
504 onSelectionChanged: (Set<int> selected) {},
505 multiSelectionEnabled: true,
506 ),
507 ),
508 ),
509 );
510
511 expect(
512 semantics,
513 hasSemantics(
514 TestSemantics.root(
515 children: <TestSemantics>[
516 // First is selected, enabled button.
517 TestSemantics(
518 flags: <SemanticsFlag>[
519 SemanticsFlag.isButton,
520 SemanticsFlag.isEnabled,
521 SemanticsFlag.hasEnabledState,
522 SemanticsFlag.hasCheckedState,
523 SemanticsFlag.isChecked,
524 SemanticsFlag.isFocusable,
525 ],
526 label: '1',
527 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
528 ),
529
530 // Second is an unselected, enabled button.
531 TestSemantics(
532 flags: <SemanticsFlag>[
533 SemanticsFlag.isButton,
534 SemanticsFlag.isEnabled,
535 SemanticsFlag.hasEnabledState,
536 SemanticsFlag.hasCheckedState,
537 SemanticsFlag.isFocusable,
538 ],
539 label: '2',
540 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
541 ),
542
543 // Third is a selected, disabled button.
544 TestSemantics(
545 flags: <SemanticsFlag>[
546 SemanticsFlag.isButton,
547 SemanticsFlag.hasEnabledState,
548 SemanticsFlag.isChecked,
549 SemanticsFlag.hasCheckedState,
550 ],
551 label: '3',
552 ),
553 ],
554 ),
555 ignoreId: true,
556 ignoreRect: true,
557 ignoreTransform: true,
558 ),
559 );
560
561 semantics.dispose();
562 });
563
564 testWidgets('SegmentedButton default overlayColor and foregroundColor resolve pressed state', (
565 WidgetTester tester,
566 ) async {
567 final ThemeData theme = ThemeData();
568
569 await tester.pumpWidget(
570 MaterialApp(
571 theme: theme,
572 home: Scaffold(
573 body: Center(
574 child: SegmentedButton<int>(
575 segments: const <ButtonSegment<int>>[
576 ButtonSegment<int>(value: 1, label: Text('1')),
577 ButtonSegment<int>(value: 2, label: Text('2')),
578 ],
579 selected: const <int>{1},
580 onSelectionChanged: (Set<int> selected) {},
581 ),
582 ),
583 ),
584 ),
585 );
586
587 final Material material = tester.widget<Material>(
588 find.descendant(of: find.byType(TextButton), matching: find.byType(Material)),
589 );
590
591 // Hovered.
592 final Offset center = tester.getCenter(find.text('2'));
593 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
594 await gesture.addPointer();
595 await gesture.moveTo(center);
596 await tester.pumpAndSettle();
597 expect(
598 getOverlayColor(tester),
599 paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)),
600 );
601 expect(material.textStyle?.color, theme.colorScheme.onSurface);
602
603 // Highlighted (pressed).
604 await gesture.down(center);
605 await tester.pumpAndSettle();
606 expect(
607 getOverlayColor(tester),
608 paints
609 ..rect()
610 ..rect(color: theme.colorScheme.onSurface.withOpacity(0.1)),
611 );
612 expect(material.textStyle?.color, theme.colorScheme.onSurface);
613 });
614
615 testWidgets('SegmentedButton has no tooltips by default', (WidgetTester tester) async {
616 final ThemeData theme = ThemeData();
617 await tester.pumpWidget(
618 MaterialApp(
619 theme: theme,
620 home: Scaffold(
621 body: Center(
622 child: SegmentedButton<int>(
623 segments: const <ButtonSegment<int>>[
624 ButtonSegment<int>(value: 1, label: Text('1')),
625 ButtonSegment<int>(value: 2, label: Text('2')),
626 ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
627 ],
628 selected: const <int>{2},
629 onSelectionChanged: (Set<int> selected) {},
630 ),
631 ),
632 ),
633 ),
634 );
635
636 expect(find.byType(Tooltip), findsNothing);
637 });
638
639 testWidgets('SegmentedButton has correct tooltips', (WidgetTester tester) async {
640 final ThemeData theme = ThemeData();
641 await tester.pumpWidget(
642 MaterialApp(
643 theme: theme,
644 home: Scaffold(
645 body: Center(
646 child: SegmentedButton<int>(
647 segments: const <ButtonSegment<int>>[
648 ButtonSegment<int>(value: 1, label: Text('1')),
649 ButtonSegment<int>(value: 2, label: Text('2'), tooltip: 't2'),
650 ButtonSegment<int>(value: 3, label: Text('3'), tooltip: 't3', enabled: false),
651 ],
652 selected: const <int>{2},
653 onSelectionChanged: (Set<int> selected) {},
654 ),
655 ),
656 ),
657 ),
658 );
659
660 expect(find.byType(Tooltip), findsNWidgets(2));
661 expect(find.byTooltip('t2'), findsOneWidget);
662 expect(find.byTooltip('t3'), findsOneWidget);
663 });
664
665 testWidgets('SegmentedButton.styleFrom is applied to the SegmentedButton', (
666 WidgetTester tester,
667 ) async {
668 const Color foregroundColor = Color(0xfffffff0);
669 const Color backgroundColor = Color(0xfffffff1);
670 const Color selectedBackgroundColor = Color(0xfffffff2);
671 const Color selectedForegroundColor = Color(0xfffffff3);
672 const Color disabledBackgroundColor = Color(0xfffffff4);
673 const Color disabledForegroundColor = Color(0xfffffff5);
674 const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
675 const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
676
677 final ButtonStyle styleFromStyle = SegmentedButton.styleFrom(
678 foregroundColor: foregroundColor,
679 backgroundColor: backgroundColor,
680 selectedForegroundColor: selectedForegroundColor,
681 selectedBackgroundColor: selectedBackgroundColor,
682 disabledForegroundColor: disabledForegroundColor,
683 disabledBackgroundColor: disabledBackgroundColor,
684 shadowColor: const Color(0xfffffff6),
685 surfaceTintColor: const Color(0xfffffff7),
686 elevation: 1,
687 textStyle: const TextStyle(color: Color(0xfffffff8)),
688 padding: const EdgeInsets.all(2),
689 side: const BorderSide(color: Color(0xfffffff9)),
690 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(3))),
691 enabledMouseCursor: enabledMouseCursor,
692 disabledMouseCursor: disabledMouseCursor,
693 visualDensity: VisualDensity.compact,
694 tapTargetSize: MaterialTapTargetSize.shrinkWrap,
695 animationDuration: const Duration(milliseconds: 100),
696 enableFeedback: true,
697 alignment: Alignment.center,
698 splashFactory: NoSplash.splashFactory,
699 );
700
701 await tester.pumpWidget(
702 MaterialApp(
703 home: Scaffold(
704 body: Center(
705 child: SegmentedButton<int>(
706 style: styleFromStyle,
707 segments: const <ButtonSegment<int>>[
708 ButtonSegment<int>(value: 1, label: Text('1')),
709 ButtonSegment<int>(value: 2, label: Text('2')),
710 ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
711 ],
712 selected: const <int>{2},
713 onSelectionChanged: (Set<int> selected) {},
714 selectedIcon: const Icon(Icons.alarm),
715 ),
716 ),
717 ),
718 ),
719 );
720
721 // Test provided button style is applied to the enabled button segment.
722 ButtonStyle? buttonStyle = tester.widget<TextButton>(find.byType(TextButton).first).style;
723 expect(buttonStyle?.foregroundColor?.resolve(enabled), foregroundColor);
724 expect(buttonStyle?.backgroundColor?.resolve(enabled), backgroundColor);
725 expect(buttonStyle?.overlayColor, styleFromStyle.overlayColor);
726 expect(buttonStyle?.surfaceTintColor, styleFromStyle.surfaceTintColor);
727 expect(buttonStyle?.elevation, styleFromStyle.elevation);
728 expect(buttonStyle?.textStyle, styleFromStyle.textStyle);
729 expect(buttonStyle?.padding, styleFromStyle.padding);
730 expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
731 expect(buttonStyle?.visualDensity, styleFromStyle.visualDensity);
732 expect(buttonStyle?.tapTargetSize, styleFromStyle.tapTargetSize);
733 expect(buttonStyle?.animationDuration, styleFromStyle.animationDuration);
734 expect(buttonStyle?.enableFeedback, styleFromStyle.enableFeedback);
735 expect(buttonStyle?.alignment, styleFromStyle.alignment);
736 expect(buttonStyle?.splashFactory, styleFromStyle.splashFactory);
737
738 // Test provided button style is applied selected button segment.
739 buttonStyle = tester.widget<TextButton>(find.byType(TextButton).at(1)).style;
740 expect(buttonStyle?.foregroundColor?.resolve(selected), selectedForegroundColor);
741 expect(buttonStyle?.backgroundColor?.resolve(selected), selectedBackgroundColor);
742 expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
743
744 // Test provided button style is applied disabled button segment.
745 buttonStyle = tester.widget<TextButton>(find.byType(TextButton).last).style;
746 expect(buttonStyle?.foregroundColor?.resolve(disabled), disabledForegroundColor);
747 expect(buttonStyle?.backgroundColor?.resolve(disabled), disabledBackgroundColor);
748 expect(buttonStyle?.mouseCursor?.resolve(disabled), disabledMouseCursor);
749
750 // Test provided button style is applied to the segmented button material.
751 final Material material = tester.widget<Material>(
752 find.descendant(of: find.byType(SegmentedButton<int>), matching: find.byType(Material)).first,
753 );
754 expect(material.elevation, styleFromStyle.elevation?.resolve(enabled));
755 expect(material.shadowColor, styleFromStyle.shadowColor?.resolve(enabled));
756 expect(material.surfaceTintColor, styleFromStyle.surfaceTintColor?.resolve(enabled));
757
758 // Test provided button style border is applied to the segmented button border.
759 expect(
760 find.byType(SegmentedButton<int>),
761 paints..line(color: styleFromStyle.side?.resolve(enabled)?.color),
762 );
763
764 // Test foreground color is applied to the overlay color.
765 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
766 await gesture.addPointer();
767 await gesture.down(tester.getCenter(find.text('1')));
768 await tester.pumpAndSettle();
769 expect(getOverlayColor(tester), paints..rect(color: foregroundColor.withOpacity(0.08)));
770 });
771
772 testWidgets('Disabled SegmentedButton has correct states when rebuilding', (
773 WidgetTester tester,
774 ) async {
775 await tester.pumpWidget(
776 MaterialApp(
777 home: Scaffold(
778 body: Center(
779 child: StatefulBuilder(
780 builder: (BuildContext context, StateSetter setState) {
781 return Column(
782 children: <Widget>[
783 SegmentedButton<int>(
784 segments: const <ButtonSegment<int>>[
785 ButtonSegment<int>(value: 0, label: Text('foo')),
786 ],
787 selected: const <int>{0},
788 ),
789 ElevatedButton(
790 onPressed: () => setState(() {}),
791 child: const Text('Trigger rebuild'),
792 ),
793 ],
794 );
795 },
796 ),
797 ),
798 ),
799 ),
800 );
801 final Set<MaterialState> states = <MaterialState>{
802 MaterialState.selected,
803 MaterialState.disabled,
804 };
805 // Check the initial states.
806 SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>));
807 expect(state.statesControllers.values.first.value, states);
808 // Trigger a rebuild.
809 await tester.tap(find.byType(ElevatedButton));
810 await tester.pumpAndSettle();
811 // Check the states after the rebuild.
812 state = tester.state(find.byType(SegmentedButton<int>));
813 expect(state.statesControllers.values.first.value, states);
814 });
815
816 testWidgets('Min button hit target height is 48.0 and min (painted) button height is 40 '
817 'by default with standard density and MaterialTapTargetSize.padded', (
818 WidgetTester tester,
819 ) async {
820 final ThemeData theme = ThemeData();
821 await tester.pumpWidget(
822 MaterialApp(
823 theme: theme,
824 home: Scaffold(
825 body: Center(
826 child: Column(
827 children: <Widget>[
828 SegmentedButton<int>(
829 segments: const <ButtonSegment<int>>[
830 ButtonSegment<int>(
831 value: 0,
832 label: Text('Day'),
833 icon: Icon(Icons.calendar_view_day),
834 ),
835 ButtonSegment<int>(
836 value: 1,
837 label: Text('Week'),
838 icon: Icon(Icons.calendar_view_week),
839 ),
840 ButtonSegment<int>(
841 value: 2,
842 label: Text('Month'),
843 icon: Icon(Icons.calendar_view_month),
844 ),
845 ButtonSegment<int>(
846 value: 3,
847 label: Text('Year'),
848 icon: Icon(Icons.calendar_today),
849 ),
850 ],
851 selected: const <int>{0},
852 onSelectionChanged: (Set<int> value) {},
853 ),
854 ],
855 ),
856 ),
857 ),
858 ),
859 );
860
861 expect(theme.visualDensity, VisualDensity.standard);
862 expect(theme.materialTapTargetSize, MaterialTapTargetSize.padded);
863
864 final Finder button = find.byType(SegmentedButton<int>);
865 expect(tester.getSize(button).height, 48.0);
866 expect(
867 find.byType(SegmentedButton<int>),
868 paints..rrect(
869 style: PaintingStyle.stroke,
870 strokeWidth: 1.0,
871 // Button border height is button.bottom(43.5) - button.top(4.5) + stoke width(1) = 40.
872 rrect: RRect.fromLTRBR(0.5, 4.5, 497.5, 43.5, const Radius.circular(19.5)),
873 ),
874 );
875 });
876
877 testWidgets(
878 'SegmentedButton expands to fill the available width when expandedInsets is not null',
879 (WidgetTester tester) async {
880 await tester.pumpWidget(
881 MaterialApp(
882 home: Scaffold(
883 body: Center(
884 child: SegmentedButton<int>(
885 segments: const <ButtonSegment<int>>[
886 ButtonSegment<int>(value: 1, label: Text('Segment 1')),
887 ButtonSegment<int>(value: 2, label: Text('Segment 2')),
888 ],
889 selected: const <int>{1},
890 expandedInsets: EdgeInsets.zero,
891 ),
892 ),
893 ),
894 ),
895 );
896
897 // Get the width of the SegmentedButton.
898 final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
899 final double segmentedButtonWidth = box.size.width;
900
901 // Get the width of the parent widget.
902 final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
903
904 // The width of the SegmentedButton must be equal to the width of the parent widget.
905 expect(segmentedButtonWidth, equals(screenWidth));
906 },
907 );
908
909 testWidgets('SegmentedButton does not expand when expandedInsets is null', (
910 WidgetTester tester,
911 ) async {
912 await tester.pumpWidget(
913 MaterialApp(
914 home: Scaffold(
915 body: Center(
916 child: SegmentedButton<int>(
917 segments: const <ButtonSegment<int>>[
918 ButtonSegment<int>(value: 1, label: Text('Segment 1')),
919 ButtonSegment<int>(value: 2, label: Text('Segment 2')),
920 ],
921 selected: const <int>{1},
922 ),
923 ),
924 ),
925 ),
926 );
927
928 // Get the width of the SegmentedButton.
929 final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
930 final double segmentedButtonWidth = box.size.width;
931
932 // Get the width of the parent widget.
933 final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
934
935 // The width of the SegmentedButton must be less than the width of the parent widget.
936 expect(segmentedButtonWidth, lessThan(screenWidth));
937 });
938
939 testWidgets('SegmentedButton.styleFrom overlayColor overrides default overlay color', (
940 WidgetTester tester,
941 ) async {
942 const Color overlayColor = Color(0xffff0000);
943 await tester.pumpWidget(
944 MaterialApp(
945 home: Scaffold(
946 body: Center(
947 child: SegmentedButton<int>(
948 style: IconButton.styleFrom(overlayColor: overlayColor),
949 segments: const <ButtonSegment<int>>[
950 ButtonSegment<int>(value: 0, label: Text('Option 1')),
951 ButtonSegment<int>(value: 1, label: Text('Option 2')),
952 ],
953 onSelectionChanged: (Set<int> selected) {},
954 selected: const <int>{1},
955 ),
956 ),
957 ),
958 ),
959 );
960
961 // Hovered selected segment,
962 Offset center = tester.getCenter(find.text('Option 1'));
963 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
964 await gesture.addPointer();
965 await gesture.moveTo(center);
966 await tester.pumpAndSettle();
967 expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08)));
968
969 // Hovered unselected segment,
970 center = tester.getCenter(find.text('Option 2'));
971 await gesture.moveTo(center);
972 await tester.pumpAndSettle();
973 expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08)));
974
975 // Highlighted unselected segment (pressed).
976 center = tester.getCenter(find.text('Option 1'));
977 await gesture.down(center);
978 await tester.pumpAndSettle();
979 expect(
980 getOverlayColor(tester),
981 paints
982 ..rect(color: overlayColor.withOpacity(0.08))
983 ..rect(color: overlayColor.withOpacity(0.1)),
984 );
985 // Remove pressed and hovered states,
986 await gesture.up();
987 await tester.pumpAndSettle();
988 await gesture.moveTo(const Offset(0, 50));
989 await tester.pumpAndSettle();
990
991 // Highlighted selected segment (pressed)
992 center = tester.getCenter(find.text('Option 2'));
993 await gesture.down(center);
994 await tester.pumpAndSettle();
995 expect(
996 getOverlayColor(tester),
997 paints
998 ..rect(color: overlayColor.withOpacity(0.08))
999 ..rect(color: overlayColor.withOpacity(0.1)),
1000 );
1001 // Remove pressed and hovered states,
1002 await gesture.up();
1003 await tester.pumpAndSettle();
1004 await gesture.moveTo(const Offset(0, 50));
1005 await tester.pumpAndSettle();
1006
1007 // Focused unselected segment.
1008 await tester.sendKeyEvent(LogicalKeyboardKey.tab);
1009 await tester.pumpAndSettle();
1010 expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.1)));
1011
1012 // Focused selected segment.
1013 await tester.sendKeyEvent(LogicalKeyboardKey.tab);
1014 await tester.pumpAndSettle();
1015 expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.1)));
1016 });
1017
1018 testWidgets('SegmentedButton.styleFrom with transparent overlayColor', (
1019 WidgetTester tester,
1020 ) async {
1021 const Color overlayColor = Colors.transparent;
1022 await tester.pumpWidget(
1023 MaterialApp(
1024 home: Scaffold(
1025 body: Center(
1026 child: SegmentedButton<int>(
1027 style: IconButton.styleFrom(overlayColor: overlayColor),
1028 segments: const <ButtonSegment<int>>[
1029 ButtonSegment<int>(value: 0, label: Text('Option')),
1030 ],
1031 onSelectionChanged: (Set<int> selected) {},
1032 selected: const <int>{0},
1033 ),
1034 ),
1035 ),
1036 ),
1037 );
1038
1039 // Hovered,
1040 final Offset center = tester.getCenter(find.text('Option'));
1041 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1042 await gesture.addPointer();
1043 await gesture.moveTo(center);
1044 await tester.pumpAndSettle();
1045 expect(getOverlayColor(tester), paints..rect(color: overlayColor));
1046
1047 // Highlighted (pressed).
1048 await gesture.down(center);
1049 await tester.pumpAndSettle();
1050 expect(
1051 getOverlayColor(tester),
1052 paints
1053 ..rect(color: overlayColor)
1054 ..rect(color: overlayColor),
1055 );
1056 // Remove pressed and hovered states,
1057 await gesture.up();
1058 await tester.pumpAndSettle();
1059 await gesture.moveTo(const Offset(0, 50));
1060 await tester.pumpAndSettle();
1061
1062 // Focused.
1063 await tester.sendKeyEvent(LogicalKeyboardKey.tab);
1064 await tester.pumpAndSettle();
1065 expect(getOverlayColor(tester), paints..rect(color: overlayColor));
1066 });
1067
1068 // This is a regression test for https://github.com/flutter/flutter/issues/144990.
1069 testWidgets('SegmentedButton clips border path when drawing segments', (
1070 WidgetTester tester,
1071 ) async {
1072 await tester.pumpWidget(
1073 MaterialApp(
1074 home: Scaffold(
1075 body: Center(
1076 child: SegmentedButton<int>(
1077 segments: const <ButtonSegment<int>>[
1078 ButtonSegment<int>(value: 0, label: Text('Option 1')),
1079 ButtonSegment<int>(value: 1, label: Text('Option 2')),
1080 ],
1081 onSelectionChanged: (Set<int> selected) {},
1082 selected: const <int>{0},
1083 ),
1084 ),
1085 ),
1086 ),
1087 );
1088
1089 expect(
1090 find.byType(SegmentedButton<int>),
1091 paints
1092 ..save()
1093 ..clipPath() // Clip the border.
1094 ..path(color: const Color(0xffe8def8)) // Draw segment 0.
1095 ..save()
1096 ..clipPath() // Clip the border.
1097 ..path(color: const Color(0x00000000)), // Draw segment 1.
1098 );
1099 });
1100
1101 // This is a regression test for https://github.com/flutter/flutter/issues/144990.
1102 testWidgets('SegmentedButton dividers matches border rect size', (WidgetTester tester) async {
1103 await tester.pumpWidget(
1104 MaterialApp(
1105 home: Scaffold(
1106 body: Center(
1107 child: SegmentedButton<int>(
1108 segments: const <ButtonSegment<int>>[
1109 ButtonSegment<int>(value: 0, label: Text('Option 1')),
1110 ButtonSegment<int>(value: 1, label: Text('Option 2')),
1111 ],
1112 onSelectionChanged: (Set<int> selected) {},
1113 selected: const <int>{0},
1114 ),
1115 ),
1116 ),
1117 ),
1118 );
1119
1120 const double tapTargetSize = 48.0;
1121 expect(
1122 find.byType(SegmentedButton<int>),
1123 paints..line(
1124 p1: const Offset(166.8000030517578, 4.0),
1125 p2: const Offset(166.8000030517578, tapTargetSize - 4.0),
1126 ),
1127 );
1128 });
1129
1130 testWidgets('SegmentedButton vertical aligned children', (WidgetTester tester) async {
1131 await tester.pumpWidget(
1132 MaterialApp(
1133 home: Scaffold(
1134 body: Center(
1135 child: SegmentedButton<int>(
1136 segments: const <ButtonSegment<int>>[
1137 ButtonSegment<int>(value: 0, label: Text('Option 0')),
1138 ButtonSegment<int>(value: 1, label: Text('Option 1')),
1139 ButtonSegment<int>(value: 2, label: Text('Option 2')),
1140 ButtonSegment<int>(value: 3, label: Text('Option 3')),
1141 ],
1142 onSelectionChanged: (Set<int> selected) {},
1143 selected: const <int>{-1}, // Prevent any of ButtonSegment to be selected
1144 direction: Axis.vertical,
1145 ),
1146 ),
1147 ),
1148 ),
1149 );
1150
1151 Rect? previewsChildRect;
1152 for (int i = 0; i <= 3; i++) {
1153 final Rect currentChildRect = tester.getRect(find.widgetWithText(TextButton, 'Option $i'));
1154 if (previewsChildRect != null) {
1155 expect(currentChildRect.left, previewsChildRect.left);
1156 expect(currentChildRect.right, previewsChildRect.right);
1157 expect(currentChildRect.top, previewsChildRect.top + previewsChildRect.height);
1158 }
1159 previewsChildRect = currentChildRect;
1160 }
1161 });
1162
1163 testWidgets('SegmentedButton vertical aligned golden image', (WidgetTester tester) async {
1164 final GlobalKey key = GlobalKey();
1165 await tester.pumpWidget(
1166 MaterialApp(
1167 home: Scaffold(
1168 body: Center(
1169 child: RepaintBoundary(
1170 key: key,
1171 child: SegmentedButton<int>(
1172 segments: const <ButtonSegment<int>>[
1173 ButtonSegment<int>(value: 0, label: Text('Option 0')),
1174 ButtonSegment<int>(value: 1, label: Text('Option 1')),
1175 ],
1176 selected: const <int>{0}, // Prevent any of ButtonSegment to be selected
1177 direction: Axis.vertical,
1178 ),
1179 ),
1180 ),
1181 ),
1182 ),
1183 );
1184
1185 await expectLater(find.byKey(key), matchesGoldenFile('segmented_button_test_vertical.png'));
1186 });
1187
1188 // Regression test for https://github.com/flutter/flutter/issues/154798.
1189 testWidgets('SegmentedButton.styleFrom can customize the button icon', (
1190 WidgetTester tester,
1191 ) async {
1192 const Color iconColor = Color(0xFFF000FF);
1193 const double iconSize = 32.0;
1194 const Color disabledIconColor = Color(0xFFFFF000);
1195 Widget buildButton({bool enabled = true}) {
1196 return MaterialApp(
1197 home: Material(
1198 child: Center(
1199 child: SegmentedButton<int>(
1200 style: SegmentedButton.styleFrom(
1201 iconColor: iconColor,
1202 iconSize: iconSize,
1203 disabledIconColor: disabledIconColor,
1204 ),
1205 segments: const <ButtonSegment<int>>[
1206 ButtonSegment<int>(value: 0, label: Text('Add'), icon: Icon(Icons.add)),
1207 ButtonSegment<int>(value: 1, label: Text('Subtract'), icon: Icon(Icons.remove)),
1208 ],
1209 showSelectedIcon: false,
1210 onSelectionChanged: enabled ? (Set<int> selected) {} : null,
1211 selected: const <int>{0},
1212 ),
1213 ),
1214 ),
1215 );
1216 }
1217
1218 // Test enabled button.
1219 await tester.pumpWidget(buildButton());
1220 expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize));
1221 expect(iconStyle(tester, Icons.add).color, iconColor);
1222
1223 // Test disabled button.
1224 await tester.pumpWidget(buildButton(enabled: false));
1225 await tester.pumpAndSettle();
1226 expect(iconStyle(tester, Icons.add).color, disabledIconColor);
1227 });
1228}
1229
1230Set<MaterialState> enabled = const <MaterialState>{};
1231Set<MaterialState> disabled = const <MaterialState>{MaterialState.disabled};
1232Set<MaterialState> selected = const <MaterialState>{MaterialState.selected};
1233