| // 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 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../widgets/semantics_tester.dart'; |
| |
| Widget boilerplate({required Widget child}) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center(child: child), |
| ); |
| } |
| |
| void main() { |
| |
| 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<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 { |
| int callbackCount = 0; |
| Set<int> 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}); |
| }); |
| |
| 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<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>{if (selected != null) 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 SemanticsTester 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.hasCheckedState, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isInMutuallyExclusiveGroup, |
| ], |
| label: '1', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| ), |
| |
| // Second is a selected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasCheckedState, |
| SemanticsFlag.isChecked, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isInMutuallyExclusiveGroup, |
| ], |
| label: '2', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| ), |
| |
| // Third is an unselected, disabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| 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<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.hasCheckedState, |
| SemanticsFlag.isChecked, |
| SemanticsFlag.isFocusable, |
| ], |
| label: '1', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| ), |
| |
| // Second is an unselected, enabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.hasCheckedState, |
| SemanticsFlag.isFocusable, |
| ], |
| label: '2', |
| actions: <SemanticsAction>[ |
| SemanticsAction.tap, |
| ], |
| ), |
| |
| // Third is a selected, disabled button. |
| TestSemantics( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isButton, |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isChecked, |
| SemanticsFlag.hasCheckedState, |
| ], |
| label: '3', |
| ), |
| ], |
| ), |
| ignoreId: true, |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| semantics.dispose(); |
| }); |
| } |