// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(['reduced-test-set']) library; import 'dart:ui'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { RenderObject getOverlayColor(WidgetTester tester) { return tester.allRenderObjects.firstWhere( (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures', ); } Widget boilerplate({required Widget child}) { return Directionality( textDirection: TextDirection.ltr, child: Center(child: child), ); } TextStyle iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)), ); return iconRichText.text.style!; } testWidgets('SegmentsButton when compositing does not crash', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/135747 // If the render object holds on to a stale canvas reference, this will // throw an exception. await tester.pumpWidget( MaterialApp( home: Scaffold( body: SegmentedButton( segments: const >[ ButtonSegment( value: 0, label: Opacity(opacity: 0.5, child: Text('option')), icon: Opacity(opacity: 0.5, child: Icon(Icons.add)), ), ], selected: const {0}, ), ), ), ); expect(find.byType(SegmentedButton), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('SegmentedButton releases state controllers for deleted segments', ( WidgetTester tester, ) async { final theme = ThemeData(); final Key key = UniqueKey(); Widget buildApp(Widget button) { return MaterialApp( theme: theme, home: Scaffold(body: Center(child: button)), ); } await tester.pumpWidget( buildApp( SegmentedButton( key: key, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ], selected: const {2}, ), ), ); await tester.pumpWidget( buildApp( SegmentedButton( key: key, segments: const >[ ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: const {2}, ), ), ); final SegmentedButtonState state = tester.state(find.byType(SegmentedButton)); expect(state.statesControllers, hasLength(2)); expect(state.statesControllers.keys.first.value, 2); expect(state.statesControllers.keys.last.value, 3); }); testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', ( WidgetTester tester, ) async { final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ), ); // Expect SegmentedButton to be built with type MaterialType.transparency. final Finder text = find.text('1'); final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first; final Finder parentMaterial = find.ancestor(of: parent, matching: find.byType(Material)).first; final Material material = tester.widget(parentMaterial); expect(material.type, MaterialType.transparency); }); testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async { var callbackCount = 0; var selectedSegment = 2; Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) { assert(selected.length == 1); selectedSegment = selected.first; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selectedSegment)); expect(selectedSegment, 2); expect(callbackCount, 0); // Tap on segment 1. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, 1); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(1)); // Tap on segment 1 again should do nothing. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, 1); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selectedSegment, 3); }); testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async { var callbackCount = 0; var selection = {1}; Widget frameWithSelection(Set selected) { return Material( child: boilerplate( child: SegmentedButton( multiSelectionEnabled: true, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: selected, onSelectionChanged: (Set selected) { selection = selected; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selection)); expect(selection, {1}); expect(callbackCount, 0); // Tap on segment 2. await tester.tap(find.text('2')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selection, {1, 2}); // Update the selection in the widget await tester.pumpWidget(frameWithSelection({1, 2})); await tester.pumpAndSettle(); // Tap on segment 1 again should remove it from selection. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selection, {2}); // Update the selection in the widget await tester.pumpWidget(frameWithSelection({2})); await tester.pumpAndSettle(); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 3); expect(selection, {2, 3}); }); // Regression test for https://github.com/flutter/flutter/issues/161922. testWidgets('Focused segment does not lose focus when its selection state changes', ( WidgetTester tester, ) async { var callbackCount = 0; var selection = {1}; Widget frameWithSelection(Set selected) { return Material( child: boilerplate( child: SegmentedButton( multiSelectionEnabled: true, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ], selected: selected, onSelectionChanged: (Set selected) { selection = selected; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selection)); expect(selection, {1}); expect(callbackCount, 0); // Select segment 2. await tester.pumpWidget(frameWithSelection({1, 2})); await tester.pumpAndSettle(); FocusNode getSegment2FocusNode() { return Focus.of(tester.element(find.text('2'))); } // Set focus on segment 2. getSegment2FocusNode().requestFocus(); await tester.pumpAndSettle(); expect(getSegment2FocusNode().hasFocus, true); // Unselect segment 2. await tester.pumpWidget(frameWithSelection({1})); await tester.pumpAndSettle(); // The button should still be focused. expect(getSegment2FocusNode().hasFocus, true); }); testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async { var callbackCount = 0; int? selectedSegment = 1; Widget frameWithSelection(int? selected) { return Material( child: boilerplate( child: SegmentedButton( emptySelectionAllowed: true, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {?selected}, onSelectionChanged: (Set selected) { selectedSegment = selected.isEmpty ? null : selected.first; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selectedSegment)); expect(selectedSegment, 1); expect(callbackCount, 0); // Tap on segment 1 should deselect it and make the selection empty. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, null); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(null)); // Tap on segment 2 should select it. await tester.tap(find.text('2')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selectedSegment, 2); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(2)); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 3); expect(selectedSegment, 3); }); testWidgets('SegmentedButton shows checkboxes for selected segments', ( WidgetTester tester, ) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder textHasIcon(String text, IconData icon) { return find.descendant(of: find.widgetWithText(Row, text), matching: find.byIcon(icon)); } await tester.pumpWidget(frameWithSelection(1)); expect(textHasIcon('1', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); }); testWidgets( 'SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, icon: Icon(Icons.add), label: Text('1')), ButtonSegment(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')), ButtonSegment(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder textHasIcon(String text, IconData icon) { return find.descendant(of: find.widgetWithText(Row, text), matching: find.byIcon(icon)); } await tester.pumpWidget(frameWithSelection(1)); expect(textHasIcon('1', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add), findsNothing); expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget); expect(textHasIcon('3', Icons.add_alarm), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('1', Icons.add), findsOneWidget); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add_a_photo), findsNothing); expect(textHasIcon('3', Icons.add_alarm), findsOneWidget); await tester.pumpWidget(frameWithSelection(3)); expect(textHasIcon('1', Icons.add), findsOneWidget); expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget); expect(textHasIcon('3', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add_alarm), findsNothing); }, ); testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', ( WidgetTester tester, ) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, icon: Icon(Icons.add)), ButtonSegment(value: 2, icon: Icon(Icons.add_a_photo)), ButtonSegment(value: 3, icon: Icon(Icons.add_alarm)), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder rowWithIcons(IconData icon1, IconData icon2) { return find.descendant(of: find.widgetWithIcon(Row, icon1), matching: find.byIcon(icon2)); } await tester.pumpWidget(frameWithSelection(1)); expect(rowWithIcons(Icons.add, Icons.check), findsOneWidget); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing); await tester.pumpWidget(frameWithSelection(2)); expect(rowWithIcons(Icons.add, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsOneWidget); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing); await tester.pumpWidget(frameWithSelection(3)); expect(rowWithIcons(Icons.add, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsOneWidget); }); testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ // First is an unselected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasSelectedState, SemanticsFlag.isFocusable, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '1', actions: [SemanticsAction.tap, SemanticsAction.focus], ), // Second is a selected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasSelectedState, SemanticsFlag.isSelected, SemanticsFlag.isFocusable, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '2', actions: [SemanticsAction.tap, SemanticsAction.focus], ), // Third is an unselected, disabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.hasSelectedState, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '3', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {1, 3}, onSelectionChanged: (Set selected) {}, multiSelectionEnabled: true, ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ // First is selected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasSelectedState, SemanticsFlag.isSelected, SemanticsFlag.isFocusable, ], label: '1', actions: [SemanticsAction.tap, SemanticsAction.focus], ), // Second is an unselected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasSelectedState, SemanticsFlag.isFocusable, ], label: '2', actions: [SemanticsAction.tap, SemanticsAction.focus], ), // Third is a selected, disabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isSelected, SemanticsFlag.hasSelectedState, ], label: '3', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); // Regression test for https://github.com/flutter/flutter/issues/146987 testWidgets('SegmentedButton announce state on all platforms', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ); // Verify that the selected segments/buttons use 'selected' semantic property. // This ensures iOS VoiceOver announces 'selected' state. final Iterable allNodes = semantics.nodesWith(); // Verify that the selected state flags are existing. final Iterable selectedNodes = allNodes.where( (SemanticsNode node) => node.hasFlag(SemanticsFlag.hasSelectedState) && node.hasFlag(SemanticsFlag.isSelected), ); expect(selectedNodes.isNotEmpty, isTrue); final Iterable unselectedNodes = allNodes.where( (SemanticsNode node) => node.hasFlag(SemanticsFlag.hasSelectedState) && !node.hasFlag(SemanticsFlag.isSelected), ); expect(unselectedNodes.isNotEmpty, isTrue); // Verify that there is one selected segment and one unselected segment. expect(selectedNodes.length, equals(1)); expect(unselectedNodes.length, equals(1)); // Ensure that the 'checked' flags are NOT used to prevent duplication issue // on Android. // On Android, TalkBack reader announces both 'checked' and 'selected' states. // This verifies `checked` state is not read with Android TalkBack. for (final node in allNodes) { expect(node.hasFlag(SemanticsFlag.hasCheckedState), isFalse); expect(node.hasFlag(SemanticsFlag.isChecked), isFalse); } semantics.dispose(); }); testWidgets('SegmentedButton default overlayColor and foregroundColor resolve pressed state', ( WidgetTester tester, ) async { final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ], selected: const {1}, onSelectionChanged: (Set selected) {}, ), ), ), ), ); final Material material = tester.widget( find.descendant(of: find.byType(TextButton).last, matching: find.byType(Material)), ); // Hovered. final Offset center = tester.getCenter(find.text('2')); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(center); await tester.pumpAndSettle(); expect( getOverlayColor(tester), paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)), ); expect(material.textStyle?.color, theme.colorScheme.onSurface); // Highlighted (pressed). await gesture.down(center); await tester.pumpAndSettle(); expect( getOverlayColor(tester), paints ..rect() ..rect(color: theme.colorScheme.onSurface.withOpacity(0.1)), ); expect(material.textStyle?.color, theme.colorScheme.onSurface); }); testWidgets('SegmentedButton has no tooltips by default', (WidgetTester tester) async { final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ), ); expect(find.byType(Tooltip), findsNothing); }); testWidgets('SegmentedButton has correct tooltips', (WidgetTester tester) async { final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2'), tooltip: 't2'), ButtonSegment(value: 3, label: Text('3'), tooltip: 't3', enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ), ); expect(find.byType(Tooltip), findsNWidgets(2)); expect(find.byTooltip('t2'), findsOneWidget); expect(find.byTooltip('t3'), findsOneWidget); }); testWidgets('SegmentedButton.styleFrom is applied to the SegmentedButton', ( WidgetTester tester, ) async { const foregroundColor = Color(0xfffffff0); const backgroundColor = Color(0xfffffff1); const selectedBackgroundColor = Color(0xfffffff2); const selectedForegroundColor = Color(0xfffffff3); const disabledBackgroundColor = Color(0xfffffff4); const disabledForegroundColor = Color(0xfffffff5); const MouseCursor enabledMouseCursor = SystemMouseCursors.text; const MouseCursor disabledMouseCursor = SystemMouseCursors.grab; final ButtonStyle styleFromStyle = SegmentedButton.styleFrom( foregroundColor: foregroundColor, backgroundColor: backgroundColor, selectedForegroundColor: selectedForegroundColor, selectedBackgroundColor: selectedBackgroundColor, disabledForegroundColor: disabledForegroundColor, disabledBackgroundColor: disabledBackgroundColor, shadowColor: const Color(0xfffffff6), surfaceTintColor: const Color(0xfffffff7), elevation: 1, textStyle: const TextStyle(color: Color(0xfffffff8)), padding: const EdgeInsets.all(2), side: const BorderSide(color: Color(0xfffffff9)), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(3))), enabledMouseCursor: enabledMouseCursor, disabledMouseCursor: disabledMouseCursor, visualDensity: VisualDensity.compact, tapTargetSize: MaterialTapTargetSize.shrinkWrap, animationDuration: const Duration(milliseconds: 100), enableFeedback: true, alignment: Alignment.center, splashFactory: NoSplash.splashFactory, ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( style: styleFromStyle, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, selectedIcon: const Icon(Icons.alarm), ), ), ), ), ); // Test provided button style is applied to the enabled button segment. ButtonStyle? buttonStyle = tester.widget(find.byType(TextButton).first).style; expect(buttonStyle?.foregroundColor?.resolve(enabled), foregroundColor); expect(buttonStyle?.backgroundColor?.resolve(enabled), backgroundColor); expect(buttonStyle?.overlayColor, styleFromStyle.overlayColor); expect(buttonStyle?.surfaceTintColor, styleFromStyle.surfaceTintColor); expect(buttonStyle?.elevation, styleFromStyle.elevation); expect(buttonStyle?.textStyle, styleFromStyle.textStyle); expect(buttonStyle?.padding, styleFromStyle.padding); expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor); expect(buttonStyle?.visualDensity, styleFromStyle.visualDensity); expect(buttonStyle?.tapTargetSize, styleFromStyle.tapTargetSize); expect(buttonStyle?.animationDuration, styleFromStyle.animationDuration); expect(buttonStyle?.enableFeedback, styleFromStyle.enableFeedback); expect(buttonStyle?.alignment, styleFromStyle.alignment); expect(buttonStyle?.splashFactory, styleFromStyle.splashFactory); // Test provided button style is applied selected button segment. buttonStyle = tester.widget(find.byType(TextButton).at(1)).style; expect(buttonStyle?.foregroundColor?.resolve(selected), selectedForegroundColor); expect(buttonStyle?.backgroundColor?.resolve(selected), selectedBackgroundColor); expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor); // Test provided button style is applied disabled button segment. buttonStyle = tester.widget(find.byType(TextButton).last).style; expect(buttonStyle?.foregroundColor?.resolve(disabled), disabledForegroundColor); expect(buttonStyle?.backgroundColor?.resolve(disabled), disabledBackgroundColor); expect(buttonStyle?.mouseCursor?.resolve(disabled), disabledMouseCursor); // Test provided button style is applied to the segmented button material. final Material material = tester.widget( find.descendant(of: find.byType(SegmentedButton), matching: find.byType(Material)).first, ); expect(material.elevation, styleFromStyle.elevation?.resolve(enabled)); expect(material.shadowColor, styleFromStyle.shadowColor?.resolve(enabled)); expect(material.surfaceTintColor, styleFromStyle.surfaceTintColor?.resolve(enabled)); // Test provided button style border is applied to the segmented button border. expect( find.byType(SegmentedButton), paints..line(color: styleFromStyle.side?.resolve(enabled)?.color), ); // Test foreground color is applied to the overlay color. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.down(tester.getCenter(find.text('1'))); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: foregroundColor.withOpacity(0.08))); }); testWidgets('Disabled SegmentedButton has correct states when rebuilding', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Column( children: [ SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('foo')), ], selected: const {0}, ), ElevatedButton( onPressed: () => setState(() {}), child: const Text('Trigger rebuild'), ), ], ); }, ), ), ), ), ); final states = {WidgetState.selected, WidgetState.disabled}; // Check the initial states. SegmentedButtonState state = tester.state(find.byType(SegmentedButton)); expect(state.statesControllers.values.first.value, states); // Trigger a rebuild. await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Check the states after the rebuild. state = tester.state(find.byType(SegmentedButton)); expect(state.statesControllers.values.first.value, states); }); testWidgets('Min button hit target height is 48.0 and min (painted) button height is 40 ' 'by default with standard density and MaterialTapTargetSize.padded', ( WidgetTester tester, ) async { final theme = ThemeData(); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: Column( children: [ SegmentedButton( segments: const >[ ButtonSegment( value: 0, label: Text('Day'), icon: Icon(Icons.calendar_view_day), ), ButtonSegment( value: 1, label: Text('Week'), icon: Icon(Icons.calendar_view_week), ), ButtonSegment( value: 2, label: Text('Month'), icon: Icon(Icons.calendar_view_month), ), ButtonSegment( value: 3, label: Text('Year'), icon: Icon(Icons.calendar_today), ), ], selected: const {0}, onSelectionChanged: (Set value) {}, ), ], ), ), ), ), ); expect(theme.visualDensity, VisualDensity.standard); expect(theme.materialTapTargetSize, MaterialTapTargetSize.padded); final Finder button = find.byType(SegmentedButton); expect(tester.getSize(button).height, 48.0); expect( find.byType(SegmentedButton), paints..rrect( style: PaintingStyle.stroke, strokeWidth: 1.0, // Button border height is button.bottom(43.5) - button.top(4.5) + stoke width(1) = 40. rrect: RRect.fromLTRBR(0.5, 4.5, 497.5, 43.5, const Radius.circular(19.5)), ), ); }); testWidgets( 'SegmentedButton expands to fill the available width when expandedInsets is not null', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('Segment 1')), ButtonSegment(value: 2, label: Text('Segment 2')), ], selected: const {1}, expandedInsets: EdgeInsets.zero, ), ), ), ), ); // Get the width of the SegmentedButton. final RenderBox box = tester.renderObject(find.byType(SegmentedButton)); final double segmentedButtonWidth = box.size.width; // Get the width of the parent widget. final double screenWidth = tester.getSize(find.byType(Scaffold)).width; // The width of the SegmentedButton must be equal to the width of the parent widget. expect(segmentedButtonWidth, equals(screenWidth)); }, ); testWidgets('SegmentedButton does not expand when expandedInsets is null', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('Segment 1')), ButtonSegment(value: 2, label: Text('Segment 2')), ], selected: const {1}, ), ), ), ), ); // Get the width of the SegmentedButton. final RenderBox box = tester.renderObject(find.byType(SegmentedButton)); final double segmentedButtonWidth = box.size.width; // Get the width of the parent widget. final double screenWidth = tester.getSize(find.byType(Scaffold)).width; // The width of the SegmentedButton must be less than the width of the parent widget. expect(segmentedButtonWidth, lessThan(screenWidth)); }); testWidgets('SegmentedButton.styleFrom overlayColor overrides default overlay color', ( WidgetTester tester, ) async { const overlayColor = Color(0xffff0000); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( style: IconButton.styleFrom(overlayColor: overlayColor), segments: const >[ ButtonSegment(value: 0, label: Text('Option 1')), ButtonSegment(value: 1, label: Text('Option 2')), ], onSelectionChanged: (Set selected) {}, selected: const {1}, ), ), ), ), ); // Hovered selected segment, Offset center = tester.getCenter(find.text('Option 1')); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(center); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08))); // Hovered unselected segment, center = tester.getCenter(find.text('Option 2')); await gesture.moveTo(center); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.08))); // Highlighted unselected segment (pressed). center = tester.getCenter(find.text('Option 1')); await gesture.down(center); await tester.pumpAndSettle(); expect( getOverlayColor(tester), paints ..rect(color: overlayColor.withOpacity(0.08)) ..rect(color: overlayColor.withOpacity(0.1)), ); // Remove pressed and hovered states, await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0, 50)); await tester.pumpAndSettle(); // Highlighted selected segment (pressed) center = tester.getCenter(find.text('Option 2')); await gesture.down(center); await tester.pumpAndSettle(); expect( getOverlayColor(tester), paints ..rect(color: overlayColor.withOpacity(0.08)) ..rect(color: overlayColor.withOpacity(0.1)), ); // Remove pressed and hovered states, await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0, 50)); await tester.pumpAndSettle(); // Focused unselected segment. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.1))); // Focused selected segment. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor.withOpacity(0.1))); }); testWidgets('SegmentedButton.styleFrom with transparent overlayColor', ( WidgetTester tester, ) async { const Color overlayColor = Colors.transparent; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( style: IconButton.styleFrom(overlayColor: overlayColor), segments: const >[ ButtonSegment(value: 0, label: Text('Option')), ], onSelectionChanged: (Set selected) {}, selected: const {0}, ), ), ), ), ); // Hovered, final Offset center = tester.getCenter(find.text('Option')); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(center); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor)); // Highlighted (pressed). await gesture.down(center); await tester.pumpAndSettle(); expect( getOverlayColor(tester), paints ..rect(color: overlayColor) ..rect(color: overlayColor), ); // Remove pressed and hovered states, await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0, 50)); await tester.pumpAndSettle(); // Focused. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); expect(getOverlayColor(tester), paints..rect(color: overlayColor)); }); // This is a regression test for https://github.com/flutter/flutter/issues/144990. testWidgets('SegmentedButton clips border path when drawing segments', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('Option 1')), ButtonSegment(value: 1, label: Text('Option 2')), ], onSelectionChanged: (Set selected) {}, selected: const {0}, ), ), ), ), ); expect( find.byType(SegmentedButton), paints ..save() ..clipPath() // Clip the border. ..path(color: const Color(0xffe8def8)) // Draw segment 0. ..save() ..clipPath() // Clip the border. ..path(color: const Color(0x00000000)), // Draw segment 1. ); }); // This is a regression test for https://github.com/flutter/flutter/issues/144990. testWidgets('SegmentedButton dividers matches border rect size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('Option 1')), ButtonSegment(value: 1, label: Text('Option 2')), ], onSelectionChanged: (Set selected) {}, selected: const {0}, ), ), ), ), ); const tapTargetSize = 48.0; expect( find.byType(SegmentedButton), paints..line( p1: const Offset(166.8000030517578, 4.0), p2: const Offset(166.8000030517578, tapTargetSize - 4.0), ), ); }); testWidgets('SegmentedButton vertical aligned children', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('Option 0')), ButtonSegment(value: 1, label: Text('Option 1')), ButtonSegment(value: 2, label: Text('Option 2')), ButtonSegment(value: 3, label: Text('Option 3')), ], onSelectionChanged: (Set selected) {}, selected: const {-1}, // Prevent any of ButtonSegment to be selected direction: Axis.vertical, ), ), ), ), ); Rect? previewsChildRect; for (var i = 0; i <= 3; i++) { final Rect currentChildRect = tester.getRect(find.widgetWithText(TextButton, 'Option $i')); if (previewsChildRect != null) { expect(currentChildRect.left, previewsChildRect.left); expect(currentChildRect.right, previewsChildRect.right); expect(currentChildRect.top, previewsChildRect.top + previewsChildRect.height); } previewsChildRect = currentChildRect; } }); testWidgets('SegmentedButton vertical aligned golden image', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RepaintBoundary( key: key, child: SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('Option 0')), ButtonSegment(value: 1, label: Text('Option 1')), ], selected: const {0}, // Prevent any of ButtonSegment to be selected direction: Axis.vertical, ), ), ), ), ), ); await expectLater(find.byKey(key), matchesGoldenFile('segmented_button_test_vertical.png')); }); // Regression test for https://github.com/flutter/flutter/issues/154798. testWidgets('SegmentedButton.styleFrom can customize the button icon', ( WidgetTester tester, ) async { const iconColor = Color(0xFFF000FF); const iconSize = 32.0; const disabledIconColor = Color(0xFFFFF000); Widget buildButton({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: SegmentedButton( style: SegmentedButton.styleFrom( iconColor: iconColor, iconSize: iconSize, disabledIconColor: disabledIconColor, ), segments: const >[ ButtonSegment(value: 0, label: Text('Add'), icon: Icon(Icons.add)), ButtonSegment(value: 1, label: Text('Subtract'), icon: Icon(Icons.remove)), ], showSelectedIcon: false, onSelectionChanged: enabled ? (Set selected) {} : null, selected: const {0}, ), ), ), ); } // Test enabled button. await tester.pumpWidget(buildButton()); expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize)); expect(iconStyle(tester, Icons.add).color, iconColor); // Test disabled button. await tester.pumpWidget(buildButton(enabled: false)); await tester.pumpAndSettle(); expect(iconStyle(tester, Icons.add).color, disabledIconColor); }); testWidgets('SegmentedButton border sides respect states', (WidgetTester tester) async { const disabledColor = Color(0XFF999999); const hoveredColor = Color(0XFF0000FF); const focusedColor = Color(0XFF00FF00); const selectedColor = Color(0XFF001234); const hoveredSelectedColor = Color(0XFF32CD32); const focusedSelectedColor = Color(0XFF0000CD); const enabledColor = Color(0XFFFF0000); Widget buildButton({ bool enabled = true, WidgetStateProperty? side, Set selected = const {}, }) { return MaterialApp( home: Material( child: Center( child: SegmentedButton( style: ButtonStyle(side: side), segments: const >[ ButtonSegment(value: 0, label: Text('Add'), icon: Icon(Icons.add)), ButtonSegment(value: 1, label: Text('Subtract'), icon: Icon(Icons.remove)), ButtonSegment( value: 2, label: Text('Multiply'), icon: Icon(Icons.multiple_stop), ), ], showSelectedIcon: false, onSelectionChanged: enabled ? (Set selected) {} : null, selected: selected, emptySelectionAllowed: true, ), ), ), ); } await tester.pumpWidget( buildButton( side: const WidgetStateProperty.fromMap({ WidgetState.hovered: BorderSide(color: hoveredColor), WidgetState.focused: BorderSide(color: focusedColor), WidgetState.any: BorderSide(color: enabledColor), }), ), ); expect(find.byType(SegmentedButton), paints..rrect(color: enabledColor)); // Hovered. Offset buttonLocation = tester.getCenter(find.text('Add')); TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(buttonLocation); addTearDown(gesture.removePointer); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: hoveredColor)); await gesture.removePointer(); await tester.pumpAndSettle(); // Focused. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: focusedColor)); await tester.pumpWidget( buildButton( side: WidgetStateProperty.fromMap({ WidgetState.hovered & WidgetState.selected: const BorderSide(color: hoveredSelectedColor), WidgetState.focused & WidgetState.selected: const BorderSide(color: focusedSelectedColor), WidgetState.hovered: const BorderSide(color: hoveredColor), WidgetState.focused: const BorderSide(color: focusedColor), WidgetState.any: const BorderSide(color: enabledColor), }), selected: {1}, ), ); await tester.pumpAndSettle(); // Hovered. buttonLocation = tester.getCenter(find.text('Add')); gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(buttonLocation); addTearDown(gesture.removePointer); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: hoveredSelectedColor)); await gesture.removePointer(); await tester.pumpAndSettle(); // Focused. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: focusedSelectedColor)); await tester.pumpWidget( buildButton( enabled: false, side: const WidgetStateProperty.fromMap({ WidgetState.disabled: BorderSide(color: disabledColor), WidgetState.any: BorderSide(color: enabledColor), }), ), ); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: disabledColor)); await tester.pumpWidget( buildButton( side: const WidgetStateProperty.fromMap({ WidgetState.selected: BorderSide(color: selectedColor), WidgetState.any: BorderSide(color: enabledColor), }), selected: {1}, ), ); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), paints..rrect(color: selectedColor)); }); testWidgets('SegmentedButton border sides respect disabled state', (WidgetTester tester) async { const disabledColor = Color(0XFF999999); const enabledColor = Color(0XFFFF0000); await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: SegmentedButton( style: const ButtonStyle( side: WidgetStateProperty.fromMap({ WidgetState.disabled: BorderSide(color: disabledColor), WidgetState.any: BorderSide(color: enabledColor), }), ), // First segment is enabled, second is disabled. segments: const >[ ButtonSegment(value: 0, label: Text('0')), ButtonSegment(value: 1, label: Text('1'), enabled: false), ], selected: const {0}, onSelectionChanged: (Set newSelection) {}, ), ), ), ), ); await tester.pumpAndSettle(); expect( find.byType(SegmentedButton), paints // First segment has an enabled border. ..rrect(color: enabledColor) // Second segment has a disabled border. ..rrect(color: disabledColor), ); }); testWidgets('SegmentedButton has expected default mouse cursor on hover', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 0, label: Text('0')), ButtonSegment(value: 1, label: Text('1')), ], selected: const {0}, onSelectionChanged: (Set newSelection) {}, ), ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: const Offset(10, 10)); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); final Offset chip = tester.getCenter(find.text('0')); await gesture.moveTo(chip); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); testWidgets('SegmentedButton has expected mouse cursor when explicitly configured', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: SegmentedButton( style: ButtonStyle( mouseCursor: WidgetStateProperty.all(SystemMouseCursors.grab), ), segments: const >[ ButtonSegment(value: 0, label: Text('0')), ButtonSegment(value: 1, label: Text('1')), ], selected: const {0}, onSelectionChanged: (Set newSelection) {}, ), ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: tester.getCenter(find.byType(SegmentedButton))); addTearDown(gesture.removePointer); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab, ); }); testWidgets('SegmentedButton does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( child: SizedBox.shrink( child: SegmentedButton( segments: const >[ ButtonSegment(value: 'X', label: Text('X')), ], selected: const {'X'}, ), ), ), ), ); expect(tester.getSize(find.byType(SegmentedButton)), Size.zero); }); } Set enabled = const {}; Set disabled = const {WidgetState.disabled}; Set selected = const {WidgetState.selected};