flutter_flutter/packages/flutter/test/material/segmented_button_test.dart
Mairramer 610a1c0cae
Fix vertical SegmentedButton not filling available width under bounded constraints (#180701)
Fixes an issue where vertical segments used intrinsic width instead of
expanding to fill the parent's bounded width, resulting in narrow tap
targets.

Fixes [#178901](https://github.com/flutter/flutter/issues/178901)

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2026-01-17 00:56:49 +00:00

1641 lines
56 KiB
Dart

// 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};