| // 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(<String>['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<RichText>( |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>( |
| value: 0, |
| label: Opacity(opacity: 0.5, child: Text('option')), |
| icon: Opacity(opacity: 0.5, child: Icon(Icons.add)), |
| ), |
| ], |
| selected: const <int>{0}, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(SegmentedButton<int>), 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<int>( |
| key: key, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ], |
| selected: const <int>{2}, |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| buildApp( |
| SegmentedButton<int>( |
| key: key, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3')), |
| ], |
| selected: const <int>{2}, |
| ), |
| ), |
| ); |
| |
| final SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>)); |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3'), enabled: false), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> 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<Material>(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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3')), |
| ], |
| selected: <int>{selected}, |
| onSelectionChanged: (Set<int> 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 = <int>{1}; |
| |
| Widget frameWithSelection(Set<int> selected) { |
| return Material( |
| child: boilerplate( |
| child: SegmentedButton<int>( |
| multiSelectionEnabled: true, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3')), |
| ], |
| selected: selected, |
| onSelectionChanged: (Set<int> selected) { |
| selection = selected; |
| callbackCount += 1; |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(frameWithSelection(selection)); |
| expect(selection, <int>{1}); |
| expect(callbackCount, 0); |
| |
| // Tap on segment 2. |
| await tester.tap(find.text('2')); |
| await tester.pumpAndSettle(); |
| expect(callbackCount, 1); |
| expect(selection, <int>{1, 2}); |
| |
| // Update the selection in the widget |
| await tester.pumpWidget(frameWithSelection(<int>{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, <int>{2}); |
| |
| // Update the selection in the widget |
| await tester.pumpWidget(frameWithSelection(<int>{2})); |
| await tester.pumpAndSettle(); |
| |
| // Tap on segment 3. |
| await tester.tap(find.text('3')); |
| await tester.pumpAndSettle(); |
| expect(callbackCount, 3); |
| expect(selection, <int>{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 = <int>{1}; |
| |
| Widget frameWithSelection(Set<int> selected) { |
| return Material( |
| child: boilerplate( |
| child: SegmentedButton<int>( |
| multiSelectionEnabled: true, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ], |
| selected: selected, |
| onSelectionChanged: (Set<int> selected) { |
| selection = selected; |
| callbackCount += 1; |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(frameWithSelection(selection)); |
| expect(selection, <int>{1}); |
| expect(callbackCount, 0); |
| |
| // Select segment 2. |
| await tester.pumpWidget(frameWithSelection(<int>{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(<int>{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<int>( |
| emptySelectionAllowed: true, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3')), |
| ], |
| selected: <int>{?selected}, |
| onSelectionChanged: (Set<int> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3')), |
| ], |
| selected: <int>{selected}, |
| onSelectionChanged: (Set<int> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, icon: Icon(Icons.add), label: Text('1')), |
| ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')), |
| ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')), |
| ], |
| selected: <int>{selected}, |
| onSelectionChanged: (Set<int> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, icon: Icon(Icons.add)), |
| ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo)), |
| ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm)), |
| ], |
| selected: <int>{selected}, |
| onSelectionChanged: (Set<int> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3'), enabled: false), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> selected) {}, |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| semantics, |
| hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| // First is an unselected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasSelectedState, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isInMutuallyExclusiveGroup, |
| ], |
| label: '1', |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
| ), |
| |
| // Second is a selected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasSelectedState, |
| SemanticsFlag.isSelected, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isInMutuallyExclusiveGroup, |
| ], |
| label: '2', |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
| ), |
| |
| // Third is an unselected, disabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3'), enabled: false), |
| ], |
| selected: const <int>{1, 3}, |
| onSelectionChanged: (Set<int> selected) {}, |
| multiSelectionEnabled: true, |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| semantics, |
| hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| // First is selected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasSelectedState, |
| SemanticsFlag.isSelected, |
| SemanticsFlag.isFocusable, |
| ], |
| label: '1', |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
| ), |
| |
| // Second is an unselected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasSelectedState, |
| SemanticsFlag.isFocusable, |
| ], |
| label: '2', |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus], |
| ), |
| |
| // Third is a selected, disabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> selected) {}, |
| ), |
| ), |
| ), |
| ); |
| |
| // Verify that the selected segments/buttons use 'selected' semantic property. |
| // This ensures iOS VoiceOver announces 'selected' state. |
| |
| final Iterable<SemanticsNode> allNodes = semantics.nodesWith(); |
| |
| // Verify that the selected state flags are existing. |
| final Iterable<SemanticsNode> selectedNodes = allNodes.where( |
| (SemanticsNode node) => |
| node.hasFlag(SemanticsFlag.hasSelectedState) && node.hasFlag(SemanticsFlag.isSelected), |
| ); |
| |
| expect(selectedNodes.isNotEmpty, isTrue); |
| |
| final Iterable<SemanticsNode> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ], |
| selected: const <int>{1}, |
| onSelectionChanged: (Set<int> selected) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Material material = tester.widget<Material>( |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3'), enabled: false), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2'), tooltip: 't2'), |
| ButtonSegment<int>(value: 3, label: Text('3'), tooltip: 't3', enabled: false), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> 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<int>( |
| style: styleFromStyle, |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ButtonSegment<int>(value: 2, label: Text('2')), |
| ButtonSegment<int>(value: 3, label: Text('3'), enabled: false), |
| ], |
| selected: const <int>{2}, |
| onSelectionChanged: (Set<int> selected) {}, |
| selectedIcon: const Icon(Icons.alarm), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Test provided button style is applied to the enabled button segment. |
| ButtonStyle? buttonStyle = tester.widget<TextButton>(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<TextButton>(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<TextButton>(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<Material>( |
| find.descendant(of: find.byType(SegmentedButton<int>), 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<int>), |
| 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: <Widget>[ |
| SegmentedButton<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('foo')), |
| ], |
| selected: const <int>{0}, |
| ), |
| ElevatedButton( |
| onPressed: () => setState(() {}), |
| child: const Text('Trigger rebuild'), |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final states = <WidgetState>{WidgetState.selected, WidgetState.disabled}; |
| // Check the initial states. |
| SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>)); |
| 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<int>)); |
| 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: <Widget>[ |
| SegmentedButton<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>( |
| value: 0, |
| label: Text('Day'), |
| icon: Icon(Icons.calendar_view_day), |
| ), |
| ButtonSegment<int>( |
| value: 1, |
| label: Text('Week'), |
| icon: Icon(Icons.calendar_view_week), |
| ), |
| ButtonSegment<int>( |
| value: 2, |
| label: Text('Month'), |
| icon: Icon(Icons.calendar_view_month), |
| ), |
| ButtonSegment<int>( |
| value: 3, |
| label: Text('Year'), |
| icon: Icon(Icons.calendar_today), |
| ), |
| ], |
| selected: const <int>{0}, |
| onSelectionChanged: (Set<int> value) {}, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(theme.visualDensity, VisualDensity.standard); |
| expect(theme.materialTapTargetSize, MaterialTapTargetSize.padded); |
| |
| final Finder button = find.byType(SegmentedButton<int>); |
| expect(tester.getSize(button).height, 48.0); |
| expect( |
| find.byType(SegmentedButton<int>), |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('Segment 1')), |
| ButtonSegment<int>(value: 2, label: Text('Segment 2')), |
| ], |
| selected: const <int>{1}, |
| expandedInsets: EdgeInsets.zero, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Get the width of the SegmentedButton. |
| final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>)); |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 1, label: Text('Segment 1')), |
| ButtonSegment<int>(value: 2, label: Text('Segment 2')), |
| ], |
| selected: const <int>{1}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Get the width of the SegmentedButton. |
| final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>)); |
| 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<int>( |
| style: IconButton.styleFrom(overlayColor: overlayColor), |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option 1')), |
| ButtonSegment<int>(value: 1, label: Text('Option 2')), |
| ], |
| onSelectionChanged: (Set<int> selected) {}, |
| selected: const <int>{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<int>( |
| style: IconButton.styleFrom(overlayColor: overlayColor), |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option')), |
| ], |
| onSelectionChanged: (Set<int> selected) {}, |
| selected: const <int>{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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option 1')), |
| ButtonSegment<int>(value: 1, label: Text('Option 2')), |
| ], |
| onSelectionChanged: (Set<int> selected) {}, |
| selected: const <int>{0}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| find.byType(SegmentedButton<int>), |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option 1')), |
| ButtonSegment<int>(value: 1, label: Text('Option 2')), |
| ], |
| onSelectionChanged: (Set<int> selected) {}, |
| selected: const <int>{0}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const tapTargetSize = 48.0; |
| expect( |
| find.byType(SegmentedButton<int>), |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option 0')), |
| ButtonSegment<int>(value: 1, label: Text('Option 1')), |
| ButtonSegment<int>(value: 2, label: Text('Option 2')), |
| ButtonSegment<int>(value: 3, label: Text('Option 3')), |
| ], |
| onSelectionChanged: (Set<int> selected) {}, |
| selected: const <int>{-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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Option 0')), |
| ButtonSegment<int>(value: 1, label: Text('Option 1')), |
| ], |
| selected: const <int>{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<int>( |
| style: SegmentedButton.styleFrom( |
| iconColor: iconColor, |
| iconSize: iconSize, |
| disabledIconColor: disabledIconColor, |
| ), |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Add'), icon: Icon(Icons.add)), |
| ButtonSegment<int>(value: 1, label: Text('Subtract'), icon: Icon(Icons.remove)), |
| ], |
| showSelectedIcon: false, |
| onSelectionChanged: enabled ? (Set<int> selected) {} : null, |
| selected: const <int>{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<BorderSide?>? side, |
| Set<int> selected = const <int>{}, |
| }) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: SegmentedButton<int>( |
| style: ButtonStyle(side: side), |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('Add'), icon: Icon(Icons.add)), |
| ButtonSegment<int>(value: 1, label: Text('Subtract'), icon: Icon(Icons.remove)), |
| ButtonSegment<int>( |
| value: 2, |
| label: Text('Multiply'), |
| icon: Icon(Icons.multiple_stop), |
| ), |
| ], |
| showSelectedIcon: false, |
| onSelectionChanged: enabled ? (Set<int> selected) {} : null, |
| selected: selected, |
| emptySelectionAllowed: true, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget( |
| buildButton( |
| side: const WidgetStateProperty<BorderSide?>.fromMap(<WidgetStatesConstraint, BorderSide?>{ |
| WidgetState.hovered: BorderSide(color: hoveredColor), |
| WidgetState.focused: BorderSide(color: focusedColor), |
| WidgetState.any: BorderSide(color: enabledColor), |
| }), |
| ), |
| ); |
| |
| expect(find.byType(SegmentedButton<int>), 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<int>), paints..rrect(color: hoveredColor)); |
| |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| |
| // Focused. |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(SegmentedButton<int>), paints..rrect(color: focusedColor)); |
| |
| await tester.pumpWidget( |
| buildButton( |
| side: WidgetStateProperty<BorderSide?>.fromMap(<WidgetStatesConstraint, BorderSide?>{ |
| 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: <int>{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<int>), paints..rrect(color: hoveredSelectedColor)); |
| |
| await gesture.removePointer(); |
| await tester.pumpAndSettle(); |
| |
| // Focused. |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(SegmentedButton<int>), paints..rrect(color: focusedSelectedColor)); |
| |
| await tester.pumpWidget( |
| buildButton( |
| enabled: false, |
| side: const WidgetStateProperty<BorderSide?>.fromMap(<WidgetStatesConstraint, BorderSide?>{ |
| WidgetState.disabled: BorderSide(color: disabledColor), |
| WidgetState.any: BorderSide(color: enabledColor), |
| }), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(SegmentedButton<int>), paints..rrect(color: disabledColor)); |
| |
| await tester.pumpWidget( |
| buildButton( |
| side: const WidgetStateProperty<BorderSide?>.fromMap(<WidgetStatesConstraint, BorderSide?>{ |
| WidgetState.selected: BorderSide(color: selectedColor), |
| WidgetState.any: BorderSide(color: enabledColor), |
| }), |
| selected: <int>{1}, |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(SegmentedButton<int>), 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<int>( |
| style: const ButtonStyle( |
| side: |
| WidgetStateProperty<BorderSide?>.fromMap(<WidgetStatesConstraint, BorderSide?>{ |
| WidgetState.disabled: BorderSide(color: disabledColor), |
| WidgetState.any: BorderSide(color: enabledColor), |
| }), |
| ), |
| // First segment is enabled, second is disabled. |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('0')), |
| ButtonSegment<int>(value: 1, label: Text('1'), enabled: false), |
| ], |
| selected: const <int>{0}, |
| onSelectionChanged: (Set<int> newSelection) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(SegmentedButton<int>), |
| 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<int>( |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('0')), |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ], |
| selected: const <int>{0}, |
| onSelectionChanged: (Set<int> 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<int>( |
| style: ButtonStyle( |
| mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.grab), |
| ), |
| segments: const <ButtonSegment<int>>[ |
| ButtonSegment<int>(value: 0, label: Text('0')), |
| ButtonSegment<int>(value: 1, label: Text('1')), |
| ], |
| selected: const <int>{0}, |
| onSelectionChanged: (Set<int> newSelection) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: tester.getCenter(find.byType(SegmentedButton<int>))); |
| 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<String>( |
| segments: const <ButtonSegment<String>>[ |
| ButtonSegment<String>(value: 'X', label: Text('X')), |
| ], |
| selected: const <String>{'X'}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(SegmentedButton<String>)), Size.zero); |
| }); |
| |
| testWidgets('SegmentedButton should expand to fill the full width', (WidgetTester tester) async { |
| tester.view |
| ..physicalSize = const Size(800, 1200) |
| ..devicePixelRatio = 1.0; |
| addTearDown(tester.view.reset); |
| |
| const double screenWidth = 800; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: SizedBox( |
| width: double.infinity, |
| child: SegmentedButton<String>( |
| direction: Axis.vertical, |
| segments: const [ |
| ButtonSegment(value: 'All', label: Text('All')), |
| ButtonSegment(value: 'Top free', label: Text('Top free')), |
| ButtonSegment(value: 'Top paid', label: Text('Top paid')), |
| ], |
| selected: const {'All'}, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| final RenderBox segmentedBox = tester.renderObject(find.byType(SegmentedButton<String>)); |
| |
| expect(segmentedBox.size.width, screenWidth); |
| |
| final Finder segmentMaterials = find.descendant( |
| of: find.byType(SegmentedButton<String>), |
| matching: find.byType(Material), |
| ); |
| |
| for (final Element element in segmentMaterials.evaluate()) { |
| final segmentBox = element.renderObject! as RenderBox; |
| |
| expect(segmentBox.size.width, screenWidth); |
| } |
| }); |
| } |
| |
| Set<WidgetState> enabled = const <WidgetState>{}; |
| Set<WidgetState> disabled = const <WidgetState>{WidgetState.disabled}; |
| Set<WidgetState> selected = const <WidgetState>{WidgetState.selected}; |