// 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. import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; Widget boilerplate({required Widget child}) { return Directionality( textDirection: TextDirection.ltr, child: Center(child: child), ); } void main() { testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) { }, ), ), ), ), ); // Expect SegmentedButton to be built with type MaterialType.transparency. final Finder text = find.text('1'); final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first; final Finder parentMaterial = find.ancestor(of: parent, matching: find.byType(Material)).first; final Material material = tester.widget(parentMaterial); expect(material.type, MaterialType.transparency); }); testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async { int callbackCount = 0; int selectedSegment = 2; Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) { assert(selected.length == 1); selectedSegment = selected.first; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selectedSegment)); expect(selectedSegment, 2); expect(callbackCount, 0); // Tap on segment 1. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, 1); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(1)); // Tap on segment 1 again should do nothing. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, 1); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selectedSegment, 3); }); testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async { int callbackCount = 0; Set selection = {1}; Widget frameWithSelection(Set selected) { return Material( child: boilerplate( child: SegmentedButton( multiSelectionEnabled: true, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: selected, onSelectionChanged: (Set selected) { selection = selected; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selection)); expect(selection, {1}); expect(callbackCount, 0); // Tap on segment 2. await tester.tap(find.text('2')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selection, {1, 2}); // Update the selection in the widget await tester.pumpWidget(frameWithSelection({1, 2})); await tester.pumpAndSettle(); // Tap on segment 1 again should remove it from selection. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selection, {2}); // Update the selection in the widget await tester.pumpWidget(frameWithSelection({2})); await tester.pumpAndSettle(); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 3); expect(selection, {2, 3}); }); testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async { int callbackCount = 0; int? selectedSegment = 1; Widget frameWithSelection(int? selected) { return Material( child: boilerplate( child: SegmentedButton( emptySelectionAllowed: true, segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {if (selected != null) selected}, onSelectionChanged: (Set selected) { selectedSegment = selected.isEmpty ? null : selected.first; callbackCount += 1; }, ), ), ); } await tester.pumpWidget(frameWithSelection(selectedSegment)); expect(selectedSegment,1); expect(callbackCount, 0); // Tap on segment 1 should deselect it and make the selection empty. await tester.tap(find.text('1')); await tester.pumpAndSettle(); expect(callbackCount, 1); expect(selectedSegment, null); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(null)); // Tap on segment 2 should select it. await tester.tap(find.text('2')); await tester.pumpAndSettle(); expect(callbackCount, 2); expect(selectedSegment, 2); // Update the selection in the widget await tester.pumpWidget(frameWithSelection(2)); // Tap on segment 3. await tester.tap(find.text('3')); await tester.pumpAndSettle(); expect(callbackCount, 3); expect(selectedSegment, 3); }); testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder textHasIcon(String text, IconData icon) { return find.descendant( of: find.widgetWithText(Row, text), matching: find.byIcon(icon) ); } await tester.pumpWidget(frameWithSelection(1)); expect(textHasIcon('1', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); }); testWidgets('SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, icon: Icon(Icons.add), label: Text('1')), ButtonSegment(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')), ButtonSegment(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder textHasIcon(String text, IconData icon) { return find.descendant( of: find.widgetWithText(Row, text), matching: find.byIcon(icon) ); } await tester.pumpWidget(frameWithSelection(1)); expect(textHasIcon('1', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add), findsNothing); expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget); expect(textHasIcon('3', Icons.add_alarm), findsOneWidget); await tester.pumpWidget(frameWithSelection(2)); expect(textHasIcon('1', Icons.add), findsOneWidget); expect(textHasIcon('2', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add_a_photo), findsNothing); expect(textHasIcon('3', Icons.add_alarm), findsOneWidget); await tester.pumpWidget(frameWithSelection(3)); expect(textHasIcon('1', Icons.add), findsOneWidget); expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget); expect(textHasIcon('3', Icons.check), findsOneWidget); expect(find.byIcon(Icons.add_alarm), findsNothing); }); testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, icon: Icon(Icons.add)), ButtonSegment(value: 2, icon: Icon(Icons.add_a_photo)), ButtonSegment(value: 3, icon: Icon(Icons.add_alarm)), ], selected: {selected}, onSelectionChanged: (Set selected) {}, ), ), ); } Finder rowWithIcons(IconData icon1, IconData icon2) { return find.descendant( of: find.widgetWithIcon(Row, icon1), matching: find.byIcon(icon2) ); } await tester.pumpWidget(frameWithSelection(1)); expect(rowWithIcons(Icons.add, Icons.check), findsOneWidget); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing); await tester.pumpWidget(frameWithSelection(2)); expect(rowWithIcons(Icons.add, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsOneWidget); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing); await tester.pumpWidget(frameWithSelection(3)); expect(rowWithIcons(Icons.add, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing); expect(rowWithIcons(Icons.add_alarm, Icons.check), findsOneWidget); }); testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) {}, ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ // First is an unselected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasCheckedState, SemanticsFlag.isFocusable, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '1', actions: [ SemanticsAction.tap, ], ), // Second is a selected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isFocusable, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '2', actions: [ SemanticsAction.tap, ], ), // Third is an unselected, disabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.hasCheckedState, SemanticsFlag.isInMutuallyExclusiveGroup, ], label: '3', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Material( child: boilerplate( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {1, 3}, onSelectionChanged: (Set selected) {}, multiSelectionEnabled: true, ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ // First is selected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isFocusable, ], label: '1', actions: [ SemanticsAction.tap, ], ), // Second is an unselected, enabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.isEnabled, SemanticsFlag.hasEnabledState, SemanticsFlag.hasCheckedState, SemanticsFlag.isFocusable, ], label: '2', actions: [ SemanticsAction.tap, ], ), // Third is a selected, disabled button. TestSemantics( flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isChecked, SemanticsFlag.hasCheckedState, ], label: '3', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('SegmentedButton default overlayColor and foregroundColor resolve pressed state', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ], selected: const {1}, onSelectionChanged: (Set selected) {}, ), ), ), ), ); RenderObject overlayColor() { return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); } final Material material = tester.widget(find.descendant( of: find.byType(TextButton), 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(overlayColor(), 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(overlayColor(), paints..rect()..rect(color: theme.colorScheme.onSurface.withOpacity(0.12))); expect(material.textStyle?.color, theme.colorScheme.onSurface); }); testWidgets('SegmentedButton has no tooltips by default', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3'), enabled: false), ], selected: const {2}, onSelectionChanged: (Set selected) { }, ), ), ), ), ); expect(find.byType(Tooltip), findsNothing); }); testWidgets('SegmentedButton has correct tooltips', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Center( child: SegmentedButton( segments: const >[ ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2'), tooltip: 't2'), ButtonSegment( value: 3, label: Text('3'), tooltip: 't3', enabled: false, ), ], selected: const {2}, onSelectionChanged: (Set selected) { }, ), ), ), ), ); expect(find.byType(Tooltip), findsNWidgets(2)); expect(find.byTooltip('t2'), findsOneWidget); expect(find.byTooltip('t3'), findsOneWidget); }); }