flutter_flutter/packages/flutter/test/material/segmented_button_theme_test.dart
Taha Tesser fa85f69e47
Add missing overlayColor property in styleFrom methods (#146685)
fixes [Add missing `overlayColor` property  in `styleFrom` methods](https://github.com/flutter/flutter/issues/146636)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

enum Sizes { extraSmall, small, medium, large, extraLarge }

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('styleFrom(overlayColor: Colors.red)', style: Theme.of(context).textTheme.titleLarge),
              TextButton(
                style: TextButton.styleFrom(overlayColor: Colors.red),
                onPressed: () {},
                child: const Text('TextButton'),
              ),
              IconButton(
                style: IconButton.styleFrom(
                  overlayColor: Colors.red,
                ),
                onPressed: () {},
                icon: const Icon(Icons.add),
              ),
              MenuBar(
                children: [
                  MenuItemButton(
                    style: MenuItemButton.styleFrom(overlayColor: Colors.red),
                    child: const Text('MenuItemButton'),
                    onPressed: () {},
                  ),
                  SubmenuButton(
                    style: SubmenuButton.styleFrom(overlayColor: Colors.red),
                    menuChildren: [
                      MenuItemButton(
                        child: const Text('MenuItemButton'),
                        onPressed: () {},
                      ),
                    ],
                    child: const Text('SubmenuButton'),
                  ),
                ],
              ),
              SegmentedButton<Sizes>(
                style:
                    SegmentedButton.styleFrom(overlayColor: Colors.red),
                segments: const <ButtonSegment<Sizes>>[
                  ButtonSegment<Sizes>(
                      value: Sizes.extraSmall, label: Text('XS')),
                  ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')),
                  ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')),
                  ButtonSegment<Sizes>(
                    value: Sizes.large,
                    label: Text('L'),
                  ),
                  ButtonSegment<Sizes>(
                      value: Sizes.extraLarge, label: Text('XL')),
                ],
                selected: const {Sizes.medium},
                onSelectionChanged: (Set<Sizes> newSelection) {},
                multiSelectionEnabled: true,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

</details>

### Preview

![ScreenRecording2024-04-12at15 25 58-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/89b9638d-f369-4ef1-b501-17c9c74b2541)
2024-04-24 11:56:32 +00:00

574 lines
23 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
RenderObject getOverlayColor(WidgetTester tester) {
return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
}
test('SegmentedButtonThemeData copyWith, ==, hashCode basics', () {
expect(const SegmentedButtonThemeData(), const SegmentedButtonThemeData().copyWith());
expect(const SegmentedButtonThemeData().hashCode, const SegmentedButtonThemeData().copyWith().hashCode);
const SegmentedButtonThemeData custom = SegmentedButtonThemeData(
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.green)),
selectedIcon: Icon(Icons.error),
);
final SegmentedButtonThemeData copy = const SegmentedButtonThemeData().copyWith(
style: custom.style,
selectedIcon: custom.selectedIcon,
);
expect(copy, custom);
});
test('SegmentedButtonThemeData lerp special cases', () {
expect(SegmentedButtonThemeData.lerp(null, null, 0), const SegmentedButtonThemeData());
const SegmentedButtonThemeData theme = SegmentedButtonThemeData();
expect(identical(SegmentedButtonThemeData.lerp(theme, theme, 0.5), theme), true);
});
testWidgets('Default SegmentedButtonThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SegmentedButtonThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('With no other configuration, defaults are used', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
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) { },
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.onSurface);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, theme.colorScheme.secondaryContainer);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.onSecondaryContainer);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('ThemeData.segmentedButtonTheme overrides defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
),
);
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) { },
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.cyan);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.purple);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.brown);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.blue);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.yellow);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('SegmentedButtonTheme overrides ThemeData and defaults', (WidgetTester tester) async {
final SegmentedButtonThemeData global = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
);
final SegmentedButtonThemeData segmentedTheme = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lightBlue;
}
if (states.contains(MaterialState.selected)) {
return Colors.lightGreen;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lime;
}
if (states.contains(MaterialState.selected)) {
return Colors.amber;
} else {
return Colors.deepPurple;
}
}),
),
selectedIcon: const Icon(Icons.plus_one),
);
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: global,
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: SegmentedButtonTheme(
data: segmentedTheme,
child: 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) { },
),
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepPurple);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.lightGreen);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.amber);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.lightBlue);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.lime);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('Widget parameters overrides SegmentedTheme, ThemeData and defaults', (WidgetTester tester) async {
final SegmentedButtonThemeData global = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
);
final SegmentedButtonThemeData segmentedTheme = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lightBlue;
}
if (states.contains(MaterialState.selected)) {
return Colors.lightGreen;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lime;
}
if (states.contains(MaterialState.selected)) {
return Colors.amber;
} else {
return Colors.deepPurple;
}
}),
),
selectedIcon: const Icon(Icons.plus_one),
);
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: global,
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: SegmentedButtonTheme(
data: segmentedTheme,
child: 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) { },
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.black12;
}
if (states.contains(MaterialState.selected)) {
return Colors.grey;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.amberAccent;
}
if (states.contains(MaterialState.selected)) {
return Colors.deepOrange;
} else {
return Colors.deepPurpleAccent;
}
}),
),
selectedIcon: const Icon(Icons.alarm),
),
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepPurpleAccent);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.grey);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepOrange);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.black12);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.amberAccent);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('SegmentedButtonTheme SegmentedButton.styleFrom overlayColor overrides default overlay color', (WidgetTester tester) async {
const Color overlayColor = Color(0xffff0000);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
segmentedButtonTheme: SegmentedButtonThemeData(
style: SegmentedButton.styleFrom(overlayColor: overlayColor),
),
),
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>{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)));
});
}